summaryrefslogtreecommitdiff
path: root/src/mongo/gotools
diff options
context:
space:
mode:
authorRamon Fernandez <ramon@mongodb.com>2017-10-11 13:40:35 -0400
committerRamon Fernandez <ramon@mongodb.com>2017-10-11 13:40:35 -0400
commit8ab02e12db5fdd017f5d8a9f8c7ee2953ba25547 (patch)
treeebf5a1304f029830ba66646eb25ed2d049c233bb /src/mongo/gotools
parent8c50e87a82afdb7852fbac32e9bb4dbe7b6f8030 (diff)
downloadmongo-8ab02e12db5fdd017f5d8a9f8c7ee2953ba25547.tar.gz
Import tools: 7067f765eb93ff5e1c601fc57317e68eda1978a5 from branch master
ref: 8bda55730d..7067f765eb for: 3.5.14 TOOLS-1109 failes to build on arm64 (syscall.Dup2 not supported) TOOLS-1542 dump and export shouldn't count views before running TOOLS-1555 Huge number of connection while replaying a record TOOLS-1567 Support MongoURI for connections TOOLS-1577 update the readme with information about mongoreplay TOOLS-1591 mongofiles should support custom GridFS files._id TOOLS-1605 Conditionally create a Windows job object in smoke.py TOOLS-1623 merge mgo's fix for "Apparent performance degradation in mongodump" TOOLS-1633 Setting SSLDBConnector.dialError causes data race warnings TOOLS-1635 Mongostat panic error TOOLS-1645 short archives cause mongorestore to hang instead of failing TOOLS-1665 Mongotools may block forever on dead connections TOOLS-1675 db.GetIndexes should not return an error when the database does not exist TOOLS-1676 Mongodump fails with OpenSSL 0.9.8 TOOLS-1679 stop running mongod's in tests with --nohttpinterface TOOLS-1685 don't mongo_version_always_use_latest when unneeded TOOLS-1694 Fix hidden options defaults TOOLS-1699 3.4.5 Server causing crash in extended_json_metadata.js TOOLS-17 mongodump --oplog should record the end oplog entry before backing up the oplog TOOLS-1704 Update mongo-tools projects to use macos-1012 distro instead of osx-1010 TOOLS-1706 mongoreplay cannot safely terminate on one core TOOLS-1713 Move mongoreplay evergreen config .evergreen.yml into common.yml TOOLS-1741 mongoimport --uri throws errors when passed Atlas Connection String URI TOOLS-1743 legacy24 and legacy26 dumprestore tests failing on master TOOLS-1767 PR: fixed typo specfy -> specify TOOLS-1774 Mongoreplay use a socket per connection instead of a session per connection TOOLS-1775 Add a filter subcommand to mongoreplay TOOLS-1779 stop building tools on solaris on all branches TOOLS-1784 add a fullspeed flag to mongoreplay that allows the tool to ignore sleeping and not synchronize timestamps TOOLS-1792 mongoreplay shouldn't output full operation dump when completing reply with full user verbosity TOOLS-1793 tool debug logging should log version and arguments
Diffstat (limited to 'src/mongo/gotools')
-rw-r--r--src/mongo/gotools/.evergreen.yml367
-rw-r--r--src/mongo/gotools/CONTRIBUTING.md6
-rw-r--r--src/mongo/gotools/Godeps6
-rw-r--r--src/mongo/gotools/README.md2
-rw-r--r--src/mongo/gotools/bsondump/main/bsondump.go2
-rw-r--r--src/mongo/gotools/common.yml52
-rw-r--r--src/mongo/gotools/common/archive/demultiplexer.go48
-rw-r--r--src/mongo/gotools/common/archive/multiplexer_roundtrip_test.go10
-rw-r--r--src/mongo/gotools/common/archive/parser.go10
-rw-r--r--src/mongo/gotools/common/archive/parser_test.go33
-rw-r--r--src/mongo/gotools/common/connstring/connstring.go398
-rw-r--r--src/mongo/gotools/common/db/connector.go31
-rw-r--r--src/mongo/gotools/common/db/db.go4
-rw-r--r--src/mongo/gotools/common/db/db_test.go71
-rw-r--r--src/mongo/gotools/common/db/namespaces.go71
-rw-r--r--src/mongo/gotools/common/db/namespaces_test.go52
-rw-r--r--src/mongo/gotools/common/db/openssl/openssl.go38
-rw-r--r--src/mongo/gotools/common/db/write_concern.go60
-rw-r--r--src/mongo/gotools/common/db/write_concern_test.go173
-rw-r--r--src/mongo/gotools/common/intents/intent.go1
-rw-r--r--src/mongo/gotools/common/intents/intent_prioritizer.go22
-rw-r--r--src/mongo/gotools/common/intents/intent_prioritizer_test.go35
-rw-r--r--src/mongo/gotools/common/options/options.go216
-rw-r--r--src/mongo/gotools/common/options/options_gssapi.go7
-rw-r--r--src/mongo/gotools/common/options/options_ssl.go9
-rw-r--r--src/mongo/gotools/common/options/options_test.go306
-rw-r--r--src/mongo/gotools/common/util/net.go24
-rw-r--r--src/mongo/gotools/import.data3
-rw-r--r--src/mongo/gotools/mongodump/main/mongodump.go11
-rw-r--r--src/mongo/gotools/mongodump/mongodump.go11
-rw-r--r--src/mongo/gotools/mongodump/oplog_dump.go15
-rw-r--r--src/mongo/gotools/mongodump/options.go11
-rw-r--r--src/mongo/gotools/mongodump/prepare.go182
-rw-r--r--src/mongo/gotools/mongoexport/main/mongoexport.go10
-rw-r--r--src/mongo/gotools/mongoexport/mongoexport.go24
-rw-r--r--src/mongo/gotools/mongofiles/main/mongofiles.go16
-rw-r--r--src/mongo/gotools/mongofiles/mongofiles.go86
-rw-r--r--src/mongo/gotools/mongofiles/mongofiles_test.go254
-rw-r--r--src/mongo/gotools/mongofiles/options.go4
-rw-r--r--src/mongo/gotools/mongofiles/options_test.go74
-rw-r--r--src/mongo/gotools/mongoimport/main/mongoimport.go11
-rw-r--r--src/mongo/gotools/mongoimport/mongoimport.go3
-rw-r--r--src/mongo/gotools/mongoimport/mongoimport_test.go23
-rw-r--r--src/mongo/gotools/mongoimport/options.go4
-rw-r--r--src/mongo/gotools/mongoimport/options_test.go74
-rw-r--r--src/mongo/gotools/mongooplog/main/mongooplog.go10
-rw-r--r--src/mongo/gotools/mongoreplay/auth_test.go23
-rw-r--r--src/mongo/gotools/mongoreplay/command_op.go6
-rw-r--r--src/mongo/gotools/mongoreplay/command_reply.go2
-rw-r--r--src/mongo/gotools/mongoreplay/delete_op.go5
-rw-r--r--src/mongo/gotools/mongoreplay/execute.go57
-rw-r--r--src/mongo/gotools/mongoreplay/execute_test.go2
-rw-r--r--src/mongo/gotools/mongoreplay/filter.go173
-rw-r--r--src/mongo/gotools/mongoreplay/filter_test.go316
-rw-r--r--src/mongo/gotools/mongoreplay/getmore_op.go5
-rw-r--r--src/mongo/gotools/mongoreplay/insert_op.go7
-rw-r--r--src/mongo/gotools/mongoreplay/killcursors_op.go5
-rw-r--r--src/mongo/gotools/mongoreplay/main/mongoreplay.go13
-rw-r--r--src/mongo/gotools/mongoreplay/mongo_op_handler.go20
-rw-r--r--src/mongo/gotools/mongoreplay/mongoreplay.go3
-rw-r--r--src/mongo/gotools/mongoreplay/monitor.go2
-rw-r--r--src/mongo/gotools/mongoreplay/op.go4
-rw-r--r--src/mongo/gotools/mongoreplay/op_msg.go2
-rw-r--r--src/mongo/gotools/mongoreplay/pcap_test.go82
-rw-r--r--src/mongo/gotools/mongoreplay/play.go203
-rw-r--r--src/mongo/gotools/mongoreplay/play_livedb_test.go72
-rw-r--r--src/mongo/gotools/mongoreplay/play_test.go57
-rw-r--r--src/mongo/gotools/mongoreplay/playbackfile.go227
-rw-r--r--src/mongo/gotools/mongoreplay/query_op.go7
-rw-r--r--src/mongo/gotools/mongoreplay/record.go44
-rw-r--r--src/mongo/gotools/mongoreplay/reply_op.go4
-rw-r--r--src/mongo/gotools/mongoreplay/stat_collector.go13
-rw-r--r--src/mongo/gotools/mongoreplay/testPcap/make_WorkloadWithEOF.js13
-rw-r--r--src/mongo/gotools/mongoreplay/update_op.go6
-rw-r--r--src/mongo/gotools/mongoreplay/util.go29
-rw-r--r--src/mongo/gotools/mongoreplay/version.go16
-rw-r--r--src/mongo/gotools/mongorestore/main/mongorestore.go13
-rw-r--r--src/mongo/gotools/mongorestore/mongorestore.go37
-rw-r--r--src/mongo/gotools/mongorestore/mongorestore_archive_test.go96
-rw-r--r--src/mongo/gotools/mongorestore/mongorestore_test.go1
-rw-r--r--src/mongo/gotools/mongorestore/options.go9
-rw-r--r--src/mongo/gotools/mongorestore/options_test.go74
-rw-r--r--src/mongo/gotools/mongorestore/testdata/test.bar.archivebin0 -> 896 bytes
-rw-r--r--src/mongo/gotools/mongostat/main/mongostat.go18
-rw-r--r--src/mongo/gotools/mongostat/stat_consumer/formatter.go2
-rw-r--r--src/mongo/gotools/mongostat/stat_consumer/grid_line_formatter.go3
-rw-r--r--src/mongo/gotools/mongostat/stat_consumer/interactive_line_formatter.go17
-rw-r--r--src/mongo/gotools/mongostat/stat_consumer/json_line_formatter.go2
-rw-r--r--src/mongo/gotools/mongotop/main/mongotop.go18
-rwxr-xr-xsrc/mongo/gotools/test.sh28
-rw-r--r--src/mongo/gotools/vendor/src/github.com/10gen/llmgo/session.go29
-rw-r--r--src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init.go7
-rw-r--r--src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_posix.go5
-rw-r--r--src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_windows.go5
-rw-r--r--src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_linux.go33
-rw-r--r--src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_other.go1
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson.go7
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson_test.go62
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/decode.go176
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/encode.go13
-rwxr-xr-xsrc/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata/update.sh26
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata_test.go241
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/decode.go8
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/extension_test.go3
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/stream_test.go2
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session.go144
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session_test.go139
-rw-r--r--src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/socket.go1
108 files changed, 3907 insertions, 1611 deletions
diff --git a/src/mongo/gotools/.evergreen.yml b/src/mongo/gotools/.evergreen.yml
deleted file mode 100644
index 43535b8124e..00000000000
--- a/src/mongo/gotools/.evergreen.yml
+++ /dev/null
@@ -1,367 +0,0 @@
-functions:
- "fetch source" :
- - command: git.get_project
- params:
- directory: src
- - command: git.apply_patch
- params:
- directory: src
-
- "build tool" :
- command: shell.exec
- params:
- working_dir: src
- script: |
- echo "Building ${tool}..."
- . ./set_gopath.sh
- go build ${args} -o bin/${tool} cmd/${tool}/main.go
-
- "upload archive" :
- command: s3.put
- params:
- aws_key: ${aws_key}
- aws_secret: ${aws_secret}
- local_file: src/${filename}
- remote_file: mongotape/archive/${build_id}/${filename}
- bucket: mciuploads
- permissions: public-read
- content_type: application/gzip
- display_name: ${filename}
-
- "create timeseries" :
- command: shell.exec
- params:
- working_dir: src
- script: |
- git clone git@github.com:10gen/support-tools
- sudo pip install argparse python-dateutil pytz
- python support-tools/timeseries/timeseries.py /data/mongotape/diagnostic.data --html timeseries.html
-
- "upload timeseries" :
- command: s3.put
- params:
- aws_key: ${aws_key}
- aws_secret: ${aws_secret}
- local_file: src/timeseries.html
- remote_file: mongotape/timeseries/${build_id}/timeseries.html
- bucket: mciuploads
- permissions: public-read
- content_type: text/html
- display_name: timeseries.html
-
- "upload tool" :
- command: s3.put
- params:
- aws_key: ${aws_key}
- aws_secret: ${aws_secret}
- local_file: src/bin/${tool}
- remote_file: mongotape/binaries/${build_id}/${tool}
- bucket: mciuploads
- permissions: public-read
- content_type: application/octet-stream
- display_name: ${tool} binary
-
- "fetch tool" :
- command: s3.get
- params:
- bucket: mciuploads
- aws_key: ${aws_key}
- aws_secret: ${aws_secret}
- local_file: src/${tool}
- remote_file: mongotape/binaries/${build_id}/${tool}
-
- "fetch pcap" :
- command: s3.get
- params:
- bucket: boxes.10gen.com
- aws_key: ${aws_key}
- aws_secret: ${aws_secret}
- local_file: src/testPcap/${pcapFname}
- remote_file: build/mongotape/${pcapFname}
-
- "fetch ftdc" :
- - command: shell.exec
- type: test
- params:
- working_dir: src
- script: |
- . ./set_gopath.sh
- go get github.com/10gen/ftdc-utils/cmd/ftdc
-
- "start mongod" :
- - command: shell.exec
- params:
- background: true
- script: |
- set -e
- set -o verbose
- cd mongodb
- echo "starting mongodb"
- mkdir -p ./mongotape_test
- ./mongod --dbpath ./mongotape_test --port=${mongod_port} ${additional_args} &
- - command: shell.exec
- params:
- script: |
- set -e
- set -o verbose
- cd mongodb
- ./mongo --nodb --eval 'assert.soon(function(x){try{var d = new Mongo("localhost:${mongod_port}"); return true}catch(e){return false}}, "timed out connecting")'
-
- "fetch mongodb" :
- - command: shell.exec
- params:
- script: |
- rm -rf mongodb
- mkdir mongodb
- cd mongodb
- curl ${mongo_url} -o mongodb.tgz
- ${decompress} mongodb.tgz
- chmod +x ./mongodb-*/bin/*
- mv ./mongodb-*/bin/* .
- rm -rf db_files
- rm -rf db_logs
- mkdir db_files
- mkdir db_logs
-
- "create auth_user" :
- - command: shell.exec
- params:
- script: |
- set -e
- set -o verbose
- cd mongodb
- ./mongo --port ${mongod_port} admin --eval "db.createUser({user:\"authorizedUser\", pwd: \"authorizedPwd\", roles:[\"readWriteAnyDatabase\", \"clusterManager\"]});"
-
- "create sharded_cluster" :
- - command: shell.exec
- params:
- background: true
- script: |
- set -e
- set -o verbose
- cd mongodb
- echo "starting mongodb"
- mkdir -p /data/db/
- ./mongo --nodb --eval 'var d = new ShardingTest({shards:3, mongos:[{port:${mongod_port}}]}); while(true){sleep(1000)}'
- - command: shell.exec
- params:
- script: |
- set -e
- set -o verbose
- cd mongodb
- ./mongo --nodb --eval 'var d; assert.soon(function(x){try{d = new Mongo("localhost:${mongod_port}"); return true} catch(e){return false}}, "timed out connection");d.setLogLevel(5, "write");'
-
- "create repl_set" :
- - command: shell.exec
- params:
- background: true
- script: |
- set -e
- set -o verbose
- cd mongodb
- echo "starting mongodb"
- mkdir -p /data/db/
- ./mongo --nodb --eval 'var repl = new ReplSetTest({nodes:3, startPort:${mongod_port}});repl.startSet();repl.initiate();repl.awaitSecondaryNodes();while(true){sleep(1000);}'
- - command: shell.exec
- params:
- script: |
- set -e
- set -o verbose
- cd mongodb
- ./mongo --nodb --eval 'assert.soon(function(x){try{var d = new Mongo("localhost:${mongod_port}"); return true} catch(e){return false}}, "timed out connection")'
-
- "run go_test" :
- - command: shell.exec
- type: test
- params:
- working_dir: src
- script: |
- . ./set_gopath.sh
- ${environment_vars} go test ${additional_args} -v > ${filename}.suite
-
-pre:
- - command: shell.track
-
-post:
- - command: shell.exec
- - command: gotest.parse_files
- params:
- files: ["src/*.suite"]
- - command: shell.cleanup
-
-tasks:
-- name: replay-dist
- commands:
- - func: "fetch source"
- - func: "build tool"
- vars:
- tool: mongotape
- - func: "upload tool"
- vars:
- tool: mongotape
- - func: "build tool"
-
-- name: replay-sanity_check
- depends_on:
- - name: replay-dist
- commands:
- - func: "fetch source"
- - func: "fetch tool"
- vars:
- tool: mongotape
- - func: "fetch mongodb"
- - func: "start mongod"
- - command: shell.exec
- params:
- working_dir: src
- script: |
- set -e
- chmod +x mongotape
- PATH=$PATH:`pwd`:`pwd`/../mongodb
- echo "Running sanity check"
- ./sanity_check.sh -p ${mongod_port}
-
-- name: replay-go_test
- depends_on:
- - name: replay-dist
- commands:
- - func: "fetch source"
- - func: "fetch tool"
- vars:
- tool: mongotape
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_multi_channel.pcap
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_single_channel.pcap
- - func: "fetch mongodb"
- - func: "start mongod"
- vars:
- mongod_port: 20000
- - func: "run go_test"
- vars:
- filename: playtest
-
-- name: replay-sharded_test
- depends_on:
- - name: replay-dist
- commands:
- - func: "fetch source"
- - func: "fetch tool"
- vars:
- tool: mongotape
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_multi_channel.pcap
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_single_channel.pcap
- - func: "fetch mongodb"
- - func: "create sharded_cluster"
- vars:
- mongod_port: 20010
- - func: "run go_test"
- vars:
- filename: sharded
- environment_vars: DB_PORT=20010
- additional_args: --run "LiveDB"
-
-- name: replay-auth_test
- depends_on:
- - name: replay-dist
- commands:
- - func: "fetch source"
- - func: "fetch tool"
- vars:
- tool: mongotape
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_multi_channel.pcap
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_single_channel.pcap
- - func: "fetch mongodb"
- - func: "start mongod"
- vars:
- mongod_port: 20000
- additional_args: --auth
- - func: "create auth_user"
- vars:
- mongod_port: 20000
- - func: "run go_test"
- vars:
- environment_vars: AUTH=1
- filename: authtest
- additional_args: --run "(LiveDB)|(Authed)"
-
-- name: replay-repl_test
- depends_on:
- - name: replay-dist
- commands:
- - func: "fetch source"
- - func: "fetch tool"
- vars:
- tool: mongotape
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_multi_channel.pcap
- - func: "fetch pcap"
- vars:
- pcapFname: getmore_single_channel.pcap
- - func: "fetch mongodb"
- - func: "create repl_set"
- vars:
- mongod_port: 20000
- - func: "run go_test"
- vars:
- filename: repltest
- additional_args: --run "LiveDB"
-
-- name: replay-replay_test
- depends_on:
- - name: replay-dist
- commands:
- - func: "fetch source"
- - func: "fetch tool"
- vars:
- tool: mongotape
- - func: "fetch mongodb"
- - func: "fetch ftdc"
- - command: shell.exec
- params:
- working_dir: src
- script: |
- set -e
- . ./set_gopath.sh
- PATH=$PATH:`pwd`:`pwd`/vendor/bin:`pwd`/../mongodb
- chmod +x mongotape
- echo "Running replay test"
- ./replay_test.sh --verbose --explicit --keep
- - command: shell.exec
- params:
- working_dir: src
- script: |
- tar czf replay.tar.gz tmp.*
- - func: "upload archive"
- vars:
- filename: replay.tar.gz
- - func: "create timeseries"
- - func: "upload timeseries"
-
-buildvariants:
-- name: ubuntu
- display_name: Ubuntu
- expansions:
- mongod_port: 27017
- mongo_url: http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.2.4.tgz
- run_on:
- - ubuntu1404-test
- tasks:
- - name: replay-dist
- - name: replay-sanity_check
- - name: replay-go_test
- - name: replay-auth_test
- - name: replay-sharded_test
- - name: replay-repl_test
- - name: replay-replay_test
diff --git a/src/mongo/gotools/CONTRIBUTING.md b/src/mongo/gotools/CONTRIBUTING.md
index 649708999de..6f7b50bdce9 100644
--- a/src/mongo/gotools/CONTRIBUTING.md
+++ b/src/mongo/gotools/CONTRIBUTING.md
@@ -7,8 +7,7 @@ For any particular improvement you want to make, you can begin a discussion on t
[MongoDB Developers Forum](https://groups.google.com/forum/?fromgroups#!forum/mongodb-dev). This is the best place to discuss your proposed improvement (and its
implementation) with the core development team.
-If you're interested in contributing, we have a list of some suggested tickets that are easy enough to get started on here:
-https://jira.mongodb.org/issues/?jql=project%20%3D%20TOOLS%20AND%20labels%20%3D%20community%20and%20status%20%3D%20open
+If you're interested in contributing, we have a list of some suggested tickets that are easy enough to get started on [here](https://jira.mongodb.org/issues/?jql=project%20%3D%20TOOLS%20AND%20labels%20%3D%20community%20and%20status%20%3D%20open)
Getting Started
---------------
@@ -17,12 +16,13 @@ Getting Started
2. Create a [Github account](https://github.com/signup/free).
3. [Fork](https://help.github.com/articles/fork-a-repo/) the repository on Github at https://github.com/mongodb/mongo-tools.
4. For more details see http://www.mongodb.org/about/contributors/.
+5. Submit a [pull request](https://help.github.com/articles/creating-a-pull-request/) against the project for review. Note: if you are a MongoDB engineer, please use the internal code review tool instead of github.
JIRA Tickets
------------
1. File a JIRA ticket in the [TOOLS project](https://jira.mongodb.org/browse/TOOLS).
-2. All commit messages to the MongoDB Tools repository must be prefaced with the relevant JIRA ticket number e.g. "TOOLS-XXX: add support for xyz".
+2. All commit messages to the MongoDB Tools repository must be prefaced with the relevant JIRA ticket number e.g. "TOOLS-XXX add support for xyz".
In filing JIRA tickets for bugs, please clearly describe the issue you are resolving, including the platforms on which the issue is present and clear steps to reproduce.
diff --git a/src/mongo/gotools/Godeps b/src/mongo/gotools/Godeps
index c5d6318b56c..8a0702bafda 100644
--- a/src/mongo/gotools/Godeps
+++ b/src/mongo/gotools/Godeps
@@ -1,4 +1,4 @@
-gopkg.in/mgo.v2 1e52f6152a9b262873f831bb5a94bcd29ef38c38 github.com/10gen/mgo
+gopkg.in/mgo.v2 39b4000d99037e917f3a3b9d2dcab667a9ef284a github.com/10gen/mgo
gopkg.in/tomb.v2 14b3d72120e8d10ea6e6b7f87f7175734b1faab8
github.com/jtolds/gls 8ddce2a84170772b95dd5d576c48d517b22cac63
github.com/jacobsa/oglematchers 3ecefc49db07722beca986d9bb71ddd026b133f0
@@ -6,8 +6,8 @@ github.com/smartystreets/assertions 287b4346dc4e71a038c346375a9d572453bc469b
github.com/smartystreets/goconvey bf58a9a1291224109919756b4dcc469c670cc7e4
github.com/jessevdk/go-flags 97448c91aac742cbca3d020b3e769013a420a06f
github.com/3rf/mongo-lint 3550fdcf1f43b89aaeabaa4559eaae6dc4407e42
-github.com/spacemonkeygo/openssl 5be686e264d836e7a01ca7fc7c53acdb8edbe768 github.com/10gen/openssl
-github.com/spacemonkeygo/spacelog ae95ccc1eb0c8ce2496c43177430efd61930f7e4
+github.com/spacemonkeygo/openssl 2869e8ca1a6eb35fb727f41611fd52b55cd0f49c github.com/10gen/openssl
+github.com/spacemonkeygo/spacelog f936fb050dc6b5fe4a96b485a6f069e8bdc59aeb
github.com/howeyc/gopass 44476384cd4721b68705e72f19e95d1a3a504370
github.com/nsf/termbox-go 0723e7c3d0a317dea811f0fbe4d6edd81908c971
github.com/mattn/go-runewidth d6bea18f789704b5f83375793155289da36a3c7f
diff --git a/src/mongo/gotools/README.md b/src/mongo/gotools/README.md
index c3e6f670c82..589e6041db9 100644
--- a/src/mongo/gotools/README.md
+++ b/src/mongo/gotools/README.md
@@ -9,6 +9,8 @@ MongoDB Tools
- **mongofiles** - _Read, write, delete, or update files in [GridFS](http://docs.mongodb.org/manual/core/gridfs/)_
- **mongooplog** - _Replay oplog entries between MongoDB servers_
- **mongotop** - _Monitor read/write activity on a mongo server_
+ - **mongoreplay** - _Capture, observe, and replay traffic for MongoDB_
+
Report any bugs, improvements, or new feature requests at https://jira.mongodb.org/browse/TOOLS
diff --git a/src/mongo/gotools/bsondump/main/bsondump.go b/src/mongo/gotools/bsondump/main/bsondump.go
index 2613d3a014a..60fa0de5461 100644
--- a/src/mongo/gotools/bsondump/main/bsondump.go
+++ b/src/mongo/gotools/bsondump/main/bsondump.go
@@ -17,7 +17,7 @@ func main() {
bsonDumpOpts := &bsondump.BSONDumpOptions{}
opts.AddOptions(bsonDumpOpts)
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'bsondump --help' for more information")
diff --git a/src/mongo/gotools/common.yml b/src/mongo/gotools/common.yml
index b43b70980e1..d0a7c28abbc 100644
--- a/src/mongo/gotools/common.yml
+++ b/src/mongo/gotools/common.yml
@@ -6,13 +6,12 @@ command_type: system
# run the same task in the previous revision if the current task fails
stepback: true
-disable_cleanup: true
mongo_tools_variables:
## List of tests to run on each buildvariant
mongo_tools_task_lists:
- osx_1010_task_list: &osx_1010_tasks
+ mac_1012_task_list: &macos_1012_tasks
- name: db
- name: dist
- name: integration
@@ -35,18 +34,10 @@ mongo_tools_variables:
# - name: replay-sharded_test
# - name: replay-repl_test
# - name: replay-replay_test
- osx_1010_ssl_task_list: &osx_1010_ssl_tasks
+ macos_1012_ssl_task_list: &macos_1012_ssl_tasks
- name: dist
- name: qa-tests
- name: native-cert-ssl
- solaris_task_list: &solaris_tasks
- - name: db
- - name: dist
- - name: integration
- - name: integration-auth
- - name: legacy28
- - name: legacy26
- - name: legacy24
ubuntu1204_task_list: &ubuntu1204_tasks
- name: db
- name: dist
@@ -1710,12 +1701,12 @@ tasks:
buildvariants:
#######################################
-# OSX Buildvariant #
+# macOS Buildvariant #
#######################################
-- name: osx-1010
- display_name: OSX 10.10 64-bit
+- name: macOS-1012
+ display_name: macOS 10.12 64-bit
run_on:
- - osx-1010
+ - macos-1012
expansions:
<<: *mongod_default_startup_args
<<: *mongo_default_startup_args
@@ -1723,12 +1714,13 @@ buildvariants:
arch: "osx/x86_64"
build_tags: "ssl"
excludes: requires_many_files
- tasks: *osx_1010_tasks
+ gorootvars: CGO_CPPFLAGS=-I/opt/mongodbtoolchain/v2/include CGO_CFLAGS=-mmacosx-version-min=10.10 CGO_LDFLAGS=-mmacosx-version-min=10.10
+ tasks: *macos_1012_tasks
-- name: osx-1010-ssl
- display_name: OSX 10.10 64-bit SSL
+- name: macOS-1012-ssl
+ display_name: macOS 10.12 64-bit SSL
run_on:
- - osx-1010
+ - macos-1012
expansions:
<<: *mongod_ssl_startup_args
<<: *mongo_ssl_startup_args
@@ -1738,7 +1730,8 @@ buildvariants:
build_tags: "ssl"
edition: ssl
excludes: requires_many_files
- tasks: *osx_1010_ssl_tasks
+ gorootvars: CGO_CPPFLAGS=-I/opt/mongodbtoolchain/v2/include CGO_CFLAGS=-mmacosx-version-min=10.10 CGO_LDFLAGS=-mmacosx-version-min=10.10
+ tasks: *macos_1012_ssl_tasks
#######################################
# Ubuntu Buildvariants #
@@ -1838,7 +1831,6 @@ buildvariants:
excludes: requires_mmap_available,requires_mongo_24,requires_mongo_26,requires_mongo_30
resmoke_args: -j 2
multiversion_override: "skip"
- mongo_version_always_use_latest: true
arch: "linux/s390x"
edition: enterprise
run_kinit: true
@@ -1862,30 +1854,12 @@ buildvariants:
excludes: requires_mmap_available,requires_large_ram,requires_mongo_24,requires_mongo_26,requires_mongo_30
resmoke_args: -j 2
multiversion_override: "skip"
- mongo_version_always_use_latest: true
arch: "linux/arm64"
edition: ssl
integration_test_args: integration
tasks: *ubuntu1604_ssl_tasks
#######################################
-# Solaris Buildvariant #
-#######################################
-- name: solaris
- display_name: Solaris 64-bit
- run_on:
- - solaris
- expansions:
- <<: *mongod_default_startup_args
- <<: *mongo_default_startup_args
- mongo_os: "sunos5"
- gorootvars: PATH="/opt/mongodbtoolchain/v2/bin/:$PATH"
- args: -gccgoflags "-lsocket -lnsl"
- excludes: requires_large_ram
- resmoke_args: -j$(kstat cpu | sort -u | grep -c "^module")
- tasks: *solaris_tasks
-
-#######################################
# Windows Buildvariants #
#######################################
- name: windows-64
diff --git a/src/mongo/gotools/common/archive/demultiplexer.go b/src/mongo/gotools/common/archive/demultiplexer.go
index 5b0503b0208..b56cc2132e2 100644
--- a/src/mongo/gotools/common/archive/demultiplexer.go
+++ b/src/mongo/gotools/common/archive/demultiplexer.go
@@ -23,6 +23,12 @@ type DemuxOut interface {
Sum64() (uint64, bool)
}
+const (
+ NamespaceUnopened = iota
+ NamespaceOpened
+ NamespaceClosed
+)
+
// Demultiplexer implements Parser.
type Demultiplexer struct {
In io.Reader
@@ -33,6 +39,20 @@ type Demultiplexer struct {
buf [db.MaxBSONSize]byte
NamespaceChan chan string
NamespaceErrorChan chan error
+ NamespaceStatus map[string]int
+}
+
+func CreateDemux(namespaceMetadatas []*CollectionMetadata, in io.Reader) *Demultiplexer {
+ demux := &Demultiplexer{
+ NamespaceStatus: make(map[string]int),
+ In: in,
+ }
+ for _, cm := range namespaceMetadatas {
+ ns := cm.Database + "." + cm.Collection
+ demux.NamespaceStatus[ns] = NamespaceUnopened
+
+ }
+ return demux
}
// Run creates and runs a parser with the Demultiplexer as a consumer
@@ -42,6 +62,7 @@ func (demux *Demultiplexer) Run() error {
if len(demux.outs) > 0 {
log.Logvf(log.Always, "demux finishing when there are still outs (%v)", len(demux.outs))
}
+
log.Logvf(log.DebugLow, "demux finishing (err:%v)", err)
return err
}
@@ -89,6 +110,10 @@ func (demux *Demultiplexer) HeaderBSON(buf []byte) error {
}
demux.currentNamespace = colHeader.Database + "." + colHeader.Collection
if _, ok := demux.outs[demux.currentNamespace]; !ok {
+ if demux.NamespaceStatus[demux.currentNamespace] != NamespaceUnopened {
+ return newError("namespace header for already opened namespace")
+ }
+ demux.NamespaceStatus[demux.currentNamespace] = NamespaceOpened
if demux.NamespaceChan != nil {
demux.NamespaceChan <- demux.currentNamespace
err := <-demux.NamespaceErrorChan
@@ -105,7 +130,11 @@ func (demux *Demultiplexer) HeaderBSON(buf []byte) error {
}
}
if colHeader.EOF {
+ if rcr, ok := demux.outs[demux.currentNamespace].(*RegularCollectionReceiver); ok {
+ rcr.err = io.EOF
+ }
demux.outs[demux.currentNamespace].Close()
+ demux.NamespaceStatus[demux.currentNamespace] = NamespaceClosed
length := int64(demux.lengths[demux.currentNamespace])
crcUInt64, ok := demux.outs[demux.currentNamespace].Sum64()
if ok {
@@ -137,18 +166,30 @@ func (demux *Demultiplexer) HeaderBSON(buf []byte) error {
// End is part of the ParserConsumer interface and receives the end of archive notification.
func (demux *Demultiplexer) End() error {
log.Logvf(log.DebugHigh, "demux End")
+ var err error
if len(demux.outs) != 0 {
openNss := []string{}
for ns := range demux.outs {
openNss = append(openNss, ns)
+ if rcr, ok := demux.outs[ns].(*RegularCollectionReceiver); ok {
+ rcr.err = newError("archive io error")
+ }
+ demux.outs[ns].Close()
+ }
+ err = newError(fmt.Sprintf("archive finished but contained files were unfinished (%v)", openNss))
+ } else {
+ for ns, status := range demux.NamespaceStatus {
+ if status != NamespaceClosed {
+ err = newError(fmt.Sprintf("archive finished before all collections were seen (%v)", ns))
+ }
}
- return newError(fmt.Sprintf("archive finished but contained files were unfinished (%v)", openNss))
}
if demux.NamespaceChan != nil {
close(demux.NamespaceChan)
}
- return nil
+
+ return err
}
// BodyBSON is part of the ParserConsumer interface and receives BSON bodies from the parser.
@@ -196,6 +237,7 @@ type RegularCollectionReceiver struct {
hash hash.Hash64
closeOnce sync.Once
openOnce sync.Once
+ err error
}
func (receiver *RegularCollectionReceiver) Sum64() (uint64, bool) {
@@ -219,7 +261,7 @@ func (receiver *RegularCollectionReceiver) Read(r []byte) (int, error) {
wLen, ok := <-receiver.readLenChan
if !ok {
close(receiver.readBufChan)
- return 0, io.EOF
+ return 0, receiver.err
}
if wLen > db.MaxBSONSize {
return 0, fmt.Errorf("incomming buffer size is too big %v", wLen)
diff --git a/src/mongo/gotools/common/archive/multiplexer_roundtrip_test.go b/src/mongo/gotools/common/archive/multiplexer_roundtrip_test.go
index 025121b704a..18fd73729db 100644
--- a/src/mongo/gotools/common/archive/multiplexer_roundtrip_test.go
+++ b/src/mongo/gotools/common/archive/multiplexer_roundtrip_test.go
@@ -86,7 +86,10 @@ func TestBasicMux(t *testing.T) {
err = <-mux.Completed
So(err, ShouldBeNil)
- demux := &Demultiplexer{In: buf}
+ demux := &Demultiplexer{
+ In: buf,
+ NamespaceStatus: make(map[string]int),
+ }
demuxOuts := map[string]*RegularCollectionReceiver{}
errChan := make(chan error)
@@ -121,7 +124,10 @@ func TestParallelMux(t *testing.T) {
mux := NewMultiplexer(writePipe, new(testNotifier))
muxIns := map[string]*MuxIn{}
- demux := &Demultiplexer{In: readPipe}
+ demux := &Demultiplexer{
+ In: readPipe,
+ NamespaceStatus: make(map[string]int),
+ }
demuxOuts := map[string]*RegularCollectionReceiver{}
inChecksum := map[string]hash.Hash{}
diff --git a/src/mongo/gotools/common/archive/parser.go b/src/mongo/gotools/common/archive/parser.go
index 2dd75807a8b..8be0529a6dc 100644
--- a/src/mongo/gotools/common/archive/parser.go
+++ b/src/mongo/gotools/common/archive/parser.go
@@ -104,8 +104,9 @@ func (parse *Parser) ReadAllBlocks(consumer ParserConsumer) (err error) {
for err == nil {
err = parse.ReadBlock(consumer)
}
+ endError := consumer.End()
if err == io.EOF {
- return nil
+ return endError
}
return err
}
@@ -119,13 +120,6 @@ func (parse *Parser) ReadAllBlocks(consumer ParserConsumer) (err error) {
// parsing failure.
func (parse *Parser) ReadBlock(consumer ParserConsumer) (err error) {
isTerminator, err := parse.readBSONOrTerminator()
- if err == io.EOF {
- handlerErr := consumer.End()
- if handlerErr != nil {
- return newParserWrappedError("ParserConsumer.End", handlerErr)
- }
- return err
- }
if err != nil {
return err
}
diff --git a/src/mongo/gotools/common/archive/parser_test.go b/src/mongo/gotools/common/archive/parser_test.go
index dba7e1d0263..bcbe2880f10 100644
--- a/src/mongo/gotools/common/archive/parser_test.go
+++ b/src/mongo/gotools/common/archive/parser_test.go
@@ -50,7 +50,7 @@ func TestParsing(t *testing.T) {
Convey("With a parser with a simple parser consumer", t, func() {
tc := &testConsumer{}
parser := Parser{}
- Convey("a well formed header and body data parse correctly", func() {
+ Convey("a well formed header and body", func() {
buf := bytes.Buffer{}
b, _ := bson.Marshal(strStruct{"header"})
buf.Write(b)
@@ -58,15 +58,24 @@ func TestParsing(t *testing.T) {
buf.Write(b)
buf.Write(term)
parser.In = &buf
- err := parser.ReadBlock(tc)
- So(err, ShouldBeNil)
- So(tc.eof, ShouldBeFalse)
- So(tc.headers[0], ShouldEqual, "header")
- So(tc.bodies[0], ShouldEqual, "body")
+ Convey("ReadBlock data parses correctly", func() {
+ err := parser.ReadBlock(tc)
+ So(err, ShouldBeNil)
+ So(tc.eof, ShouldBeFalse)
+ So(tc.headers[0], ShouldEqual, "header")
+ So(tc.bodies[0], ShouldEqual, "body")
- err = parser.ReadBlock(tc)
- So(err, ShouldEqual, io.EOF)
- So(tc.eof, ShouldBeTrue)
+ err = parser.ReadBlock(tc)
+ So(err, ShouldEqual, io.EOF)
+ })
+ Convey("ReadAllBlock data parses correctly", func() {
+ err := parser.ReadAllBlocks(tc)
+ So(err, ShouldEqual, nil)
+ So(tc.eof, ShouldBeTrue)
+ So(tc.headers[0], ShouldEqual, "header")
+ So(tc.bodies[0], ShouldEqual, "body")
+
+ })
})
Convey("a well formed header and multiple body datas parse correctly", func() {
buf := bytes.Buffer{}
@@ -90,7 +99,7 @@ func TestParsing(t *testing.T) {
err = parser.ReadBlock(tc)
So(err, ShouldEqual, io.EOF)
- So(tc.eof, ShouldBeTrue)
+ So(tc.eof, ShouldBeFalse)
})
Convey("an incorrect terminator should cause an error", func() {
buf := bytes.Buffer{}
@@ -108,13 +117,13 @@ func TestParsing(t *testing.T) {
parser.In = &buf
err := parser.ReadBlock(tc)
So(err, ShouldEqual, io.EOF)
- So(tc.eof, ShouldBeTrue)
+ So(tc.eof, ShouldBeFalse)
})
Convey("an error comming from the consumer should propigate through the parser", func() {
tc.eof = true
buf := bytes.Buffer{}
parser.In = &buf
- err := parser.ReadBlock(tc)
+ err := parser.ReadAllBlocks(tc)
So(err.Error(), ShouldContainSubstring, "double end")
})
Convey("a partial block should result in a non-EOF error", func() {
diff --git a/src/mongo/gotools/common/connstring/connstring.go b/src/mongo/gotools/common/connstring/connstring.go
new file mode 100644
index 00000000000..d1da3e87514
--- /dev/null
+++ b/src/mongo/gotools/common/connstring/connstring.go
@@ -0,0 +1,398 @@
+package connstring
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Parse parses the provided uri and returns a URI object.
+func ParseURIConnectionString(s string) (ConnString, error) {
+ var p parser
+ err := p.parse(s)
+ if err != nil {
+ err = fmt.Errorf("error parsing uri (%s): %s", s, err)
+ }
+ return p.ConnString, err
+}
+
+// ConnString represents a connection string to mongodb.
+type ConnString struct {
+ Original string
+ AppName string
+ AuthMechanism string
+ AuthMechanismProperties map[string]string
+ AuthSource string
+ Connect ConnectMode
+ ConnectTimeout time.Duration
+ Database string
+ FSync bool
+ HeartbeatInterval time.Duration
+ Hosts []string
+ Journal bool
+ KerberosService string
+ KerberosServiceHost string
+ MaxConnIdleTime time.Duration
+ MaxConnLifeTime time.Duration
+ MaxConnsPerHost uint16
+ MaxConnsPerHostSet bool
+ MaxIdleConnsPerHost uint16
+ MaxIdleConnsPerHostSet bool
+ Password string
+ PasswordSet bool
+ ReadPreference string
+ ReadPreferenceTagSets []map[string]string
+ ReplicaSet string
+ ServerSelectionTimeout time.Duration
+ SocketTimeout time.Duration
+ Username string
+ UseSSL bool
+ W string
+ WTimeout time.Duration
+
+ Options map[string][]string
+ UnknownOptions map[string][]string
+}
+
+func (u *ConnString) String() string {
+ return u.Original
+}
+
+// ConnectMode informs the driver on how to connect
+// to the server.
+type ConnectMode uint8
+
+// ConnectMode constants.
+const (
+ AutoConnect ConnectMode = iota
+ SingleConnect
+)
+
+type parser struct {
+ ConnString
+
+ haveWTimeoutMS bool
+}
+
+func (p *parser) parse(original string) error {
+ p.Original = original
+
+ uri := original
+ var err error
+ // scheme
+ if !strings.HasPrefix(uri, "mongodb://") {
+ return fmt.Errorf("scheme must be \"mongodb\"")
+ }
+
+ // user info
+ uri = uri[10:]
+
+ if idx := strings.Index(uri, "@"); idx != -1 {
+ userInfo := uri[:idx]
+ uri = uri[idx+1:]
+
+ username := userInfo
+ var password string
+
+ if idx := strings.Index(userInfo, ":"); idx != -1 {
+ username = userInfo[:idx]
+ password = userInfo[idx+1:]
+ }
+
+ p.Username, err = url.QueryUnescape(username)
+ if err != nil {
+ return fmt.Errorf("invalid username: %s", err)
+ }
+ if len(password) > 1 {
+ if strings.Contains(password, ":") {
+ return fmt.Errorf("unescaped colon in password")
+ }
+ p.Password, err = url.QueryUnescape(password)
+ if err != nil {
+ return fmt.Errorf("invalid password: %s", err)
+ }
+ p.PasswordSet = true
+ }
+
+ }
+
+ // hosts
+ hosts := uri
+ if idx := strings.IndexAny(uri, "/?@"); idx != -1 {
+ if uri[idx] == '@' {
+ return fmt.Errorf("unescaped @ sign in user info")
+ }
+ if uri[idx] == '?' {
+ return fmt.Errorf("must have a / before the query ?")
+ }
+
+ hosts = uri[:idx]
+ }
+
+ for _, host := range strings.Split(hosts, ",") {
+ err = p.addHost(host)
+ if err != nil {
+ return fmt.Errorf("invalid host \"%s\": %s", host, err)
+ }
+ }
+
+ if len(p.Hosts) == 0 {
+ return fmt.Errorf("must have at least 1 host")
+ }
+
+ uri = uri[len(hosts):]
+
+ if len(uri) == 0 {
+ return nil
+ }
+
+ if uri[0] != '/' {
+ return fmt.Errorf("must have a / separator between hosts and path")
+ }
+
+ uri = uri[1:]
+ if len(uri) == 0 {
+ return nil
+ }
+
+ database := uri
+ if idx := strings.IndexAny(uri, "?"); idx != -1 {
+ database = uri[:idx]
+ }
+
+ p.Database, err = url.QueryUnescape(database)
+ if err != nil {
+ return fmt.Errorf("invalid database \"%s\": %s", database, err)
+ }
+
+ uri = uri[len(database):]
+
+ if len(uri) == 0 {
+ return nil
+ }
+
+ if uri[0] != '?' {
+ return fmt.Errorf("must have a ? separator between path and query")
+ }
+
+ uri = uri[1:]
+ if len(uri) == 0 {
+ return nil
+ }
+
+ for _, pair := range strings.FieldsFunc(uri, func(r rune) bool { return r == ';' || r == '&' }) {
+ err = p.addOption(pair)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (p *parser) addHost(host string) error {
+ if host == "" {
+ return nil
+ }
+ host, err := url.QueryUnescape(host)
+ if err != nil {
+ return fmt.Errorf("invalid host \"%s\": %s", host, err)
+ }
+
+ _, port, err := net.SplitHostPort(host)
+ // this is unfortunate that SplitHostPort actually requires
+ // a port to exist.
+ if err != nil {
+ if addrError, ok := err.(*net.AddrError); !ok || addrError.Err != "missing port in address" {
+ return err
+ }
+ }
+
+ if port != "" {
+ d, err := strconv.Atoi(port)
+ if err != nil {
+ return fmt.Errorf("port must be an integer: %s", err)
+ }
+ if d <= 0 || d >= 65536 {
+ return fmt.Errorf("port must be in the range [1, 65535]")
+ }
+ }
+ p.Hosts = append(p.Hosts, host)
+ return nil
+}
+
+func (p *parser) addOption(pair string) error {
+ kv := strings.SplitN(pair, "=", 2)
+ if len(kv) != 2 || kv[0] == "" {
+ return fmt.Errorf("invalid option")
+ }
+
+ key, err := url.QueryUnescape(kv[0])
+ if err != nil {
+ return fmt.Errorf("invalid option key \"%s\": %s", kv[0], err)
+ }
+
+ value, err := url.QueryUnescape(kv[1])
+ if err != nil {
+ return fmt.Errorf("invalid option value \"%s\": %s", kv[1], err)
+ }
+
+ lowerKey := strings.ToLower(key)
+ switch lowerKey {
+ case "appname":
+ p.AppName = value
+ case "authmechanism":
+ p.AuthMechanism = value
+ case "authmechanismproperties":
+ p.AuthMechanismProperties = make(map[string]string)
+ pairs := strings.Split(value, ",")
+ for _, pair := range pairs {
+ kv := strings.SplitN(pair, ":", 2)
+ if len(kv) != 2 || kv[0] == "" {
+ return fmt.Errorf("invalid authMechanism property")
+ }
+ p.AuthMechanismProperties[kv[0]] = kv[1]
+ }
+ case "authsource":
+ p.AuthSource = value
+ case "connect":
+ switch strings.ToLower(value) {
+ case "auto", "automatic":
+ p.Connect = AutoConnect
+ case "direct", "single":
+ p.Connect = SingleConnect
+ default:
+ return fmt.Errorf("invalid 'connect' value: %s", value)
+ }
+ case "connecttimeoutms":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.ConnectTimeout = time.Duration(n) * time.Millisecond
+ case "heartbeatintervalms", "heartbeatfrequencyms":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.HeartbeatInterval = time.Duration(n) * time.Millisecond
+ case "fsync":
+ f, err := strconv.ParseBool(value)
+ if err != nil {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.FSync = f
+ case "j":
+ j, err := strconv.ParseBool(value)
+ if err != nil {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.Journal = j
+ case "gssapiservicename":
+ p.KerberosService = value
+ case "gssapihostname":
+ p.KerberosServiceHost = value
+ case "maxconnsperhost":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.MaxConnsPerHost = uint16(n)
+ p.MaxConnsPerHostSet = true
+ case "maxidleconnsperhost":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.MaxIdleConnsPerHost = uint16(n)
+ p.MaxIdleConnsPerHostSet = true
+ case "maxidletimems":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.MaxConnIdleTime = time.Duration(n) * time.Millisecond
+ case "maxlifetimems":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.MaxConnLifeTime = time.Duration(n) * time.Millisecond
+ case "maxpoolsize":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.MaxConnsPerHost = uint16(n)
+ p.MaxConnsPerHostSet = true
+ p.MaxIdleConnsPerHost = uint16(n)
+ p.MaxIdleConnsPerHostSet = true
+ case "readpreference":
+ p.ReadPreference = value
+ case "readpreferencetags":
+ tags := make(map[string]string)
+ items := strings.Split(value, ",")
+ for _, item := range items {
+ parts := strings.Split(item, ":")
+ if len(parts) != 2 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ tags[parts[0]] = parts[1]
+ }
+ p.ReadPreferenceTagSets = append(p.ReadPreferenceTagSets, tags)
+ case "replicaset":
+ p.ReplicaSet = value
+ case "serverselectiontimeoutms":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.ServerSelectionTimeout = time.Duration(n) * time.Millisecond
+ case "sockettimeoutms":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.SocketTimeout = time.Duration(n) * time.Millisecond
+ case "ssl":
+ b, err := strconv.ParseBool(value)
+ if err != nil {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.UseSSL = b
+ case "w":
+ p.W = value
+ case "wtimeoutms":
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.WTimeout = time.Duration(n) * time.Millisecond
+ p.haveWTimeoutMS = true
+ case "wtimeout":
+ if p.haveWTimeoutMS {
+ // use wtimeoutMS if it exists
+ break
+ }
+ n, err := strconv.Atoi(value)
+ if err != nil || n < 0 {
+ return fmt.Errorf("invalid value for %s: %s", key, value)
+ }
+ p.WTimeout = time.Duration(n) * time.Millisecond
+ default:
+ if p.UnknownOptions == nil {
+ p.UnknownOptions = make(map[string][]string)
+ }
+ p.UnknownOptions[lowerKey] = append(p.UnknownOptions[lowerKey], value)
+ }
+
+ if p.Options == nil {
+ p.Options = make(map[string][]string)
+ }
+ p.Options[lowerKey] = append(p.Options[lowerKey], value)
+
+ return nil
+}
diff --git a/src/mongo/gotools/common/db/connector.go b/src/mongo/gotools/common/db/connector.go
index c3e50ddfa5d..47895314983 100644
--- a/src/mongo/gotools/common/db/connector.go
+++ b/src/mongo/gotools/common/db/connector.go
@@ -1,6 +1,7 @@
package db
import (
+ "net"
"time"
"github.com/mongodb/mongo-tools/common/db/kerberos"
@@ -27,23 +28,43 @@ type VanillaDBConnector struct {
// connection string and then sets up the dial information using the default
// dial timeout.
func (self *VanillaDBConnector) Configure(opts options.ToolOptions) error {
- // create the addresses to be used to connect
- connectionAddrs := util.CreateConnectionAddrs(opts.Host, opts.Port)
-
timeout := time.Duration(opts.Timeout) * time.Second
+ // create the dialer func that will be used to connect
+ dialer := func(addr *mgo.ServerAddr) (net.Conn, error) {
+ conn, err := net.DialTimeout("tcp", addr.String(), timeout)
+ if err != nil {
+ return nil, err
+ }
+ // enable TCP keepalive
+ err = util.EnableTCPKeepAlive(conn, time.Duration(opts.TCPKeepAliveSeconds)*time.Second)
+ if err != nil {
+ return nil, err
+ }
+ return conn, nil
+ }
+
// set up the dial info
self.dialInfo = &mgo.DialInfo{
- Addrs: connectionAddrs,
- Timeout: timeout,
Direct: opts.Direct,
ReplicaSetName: opts.ReplicaSetName,
Username: opts.Auth.Username,
Password: opts.Auth.Password,
Source: opts.GetAuthenticationDatabase(),
Mechanism: opts.Auth.Mechanism,
+ DialServer: dialer,
+ Timeout: timeout,
}
+
+ // create or fetch the addresses to be used to connect
+ if opts.URI != nil && opts.URI.ConnectionString != "" {
+ self.dialInfo.Addrs = opts.URI.GetConnectionAddrs()
+ } else {
+ self.dialInfo.Addrs = util.CreateConnectionAddrs(opts.Host, opts.Port)
+ }
+
kerberos.AddKerberosOpts(opts, self.dialInfo)
+
return nil
}
diff --git a/src/mongo/gotools/common/db/db.go b/src/mongo/gotools/common/db/db.go
index 5372cb6981f..519b257bafc 100644
--- a/src/mongo/gotools/common/db/db.go
+++ b/src/mongo/gotools/common/db/db.go
@@ -120,7 +120,9 @@ func (self *SessionProvider) GetSession() (*mgo.Session, error) {
func (self *SessionProvider) Close() {
self.masterSessionLock.Lock()
defer self.masterSessionLock.Unlock()
- self.masterSession.Close()
+ if self.masterSession != nil {
+ self.masterSession.Close()
+ }
}
// refresh is a helper for modifying the session based on the
diff --git a/src/mongo/gotools/common/db/db_test.go b/src/mongo/gotools/common/db/db_test.go
index b266d8619d8..3ad568164ca 100644
--- a/src/mongo/gotools/common/db/db_test.go
+++ b/src/mongo/gotools/common/db/db_test.go
@@ -4,6 +4,7 @@ import (
"github.com/mongodb/mongo-tools/common/options"
"github.com/mongodb/mongo-tools/common/testutil"
. "github.com/smartystreets/goconvey/convey"
+ "gopkg.in/mgo.v2"
"reflect"
"testing"
)
@@ -28,6 +29,10 @@ func TestNewSessionProvider(t *testing.T) {
So(reflect.TypeOf(provider.connector), ShouldEqual,
reflect.TypeOf(&VanillaDBConnector{}))
+ Convey("and should be closeable", func() {
+ provider.Close()
+ })
+
})
Convey("the master session should be successfully "+
@@ -60,6 +65,72 @@ func TestNewSessionProvider(t *testing.T) {
}
+func TestGetIndexes(t *testing.T) {
+
+ testutil.VerifyTestType(t, "db")
+
+ Convey("With a valid session", t, func() {
+ opts := options.ToolOptions{
+ Connection: &options.Connection{
+ Port: DefaultTestPort,
+ },
+ SSL: &options.SSL{},
+ Auth: &options.Auth{},
+ }
+ provider, err := NewSessionProvider(opts)
+ So(err, ShouldBeNil)
+ session, err := provider.GetSession()
+ So(err, ShouldBeNil)
+
+ existing := session.DB("exists").C("collection")
+ missing := session.DB("exists").C("missing")
+ missingDB := session.DB("missingDB").C("missingCollection")
+
+ err = existing.Database.DropDatabase()
+ So(err, ShouldBeNil)
+ err = existing.Create(&mgo.CollectionInfo{})
+ So(err, ShouldBeNil)
+ err = missingDB.Database.DropDatabase()
+ So(err, ShouldBeNil)
+
+ Convey("When GetIndexes is called on", func() {
+ Convey("an existing collection there should be no error", func() {
+ indexesIter, err := GetIndexes(existing)
+ So(err, ShouldBeNil)
+ Convey("and indexes should be returned", func() {
+ So(indexesIter, ShouldNotBeNil)
+ var indexes []mgo.Index
+ err := indexesIter.All(&indexes)
+ So(err, ShouldBeNil)
+ So(len(indexes), ShouldBeGreaterThan, 0)
+ })
+ })
+
+ Convey("a missing collection there should be no error", func() {
+ indexesIter, err := GetIndexes(missing)
+ So(err, ShouldBeNil)
+ Convey("and there should be no indexes", func() {
+ So(indexesIter, ShouldBeNil)
+ })
+ })
+
+ Convey("a missing database there should be no error", func() {
+ indexesIter, err := GetIndexes(missingDB)
+ So(err, ShouldBeNil)
+ Convey("and there should be no indexes", func() {
+ So(indexesIter, ShouldBeNil)
+ })
+ })
+ })
+
+ Reset(func() {
+ existing.Database.DropDatabase()
+ session.Close()
+ provider.Close()
+ })
+ })
+}
+
type listDatabasesCommand struct {
Databases []map[string]interface{} `json:"databases"`
Ok bool `json:"ok"`
diff --git a/src/mongo/gotools/common/db/namespaces.go b/src/mongo/gotools/common/db/namespaces.go
index 908687b1c56..4a2de142412 100644
--- a/src/mongo/gotools/common/db/namespaces.go
+++ b/src/mongo/gotools/common/db/namespaces.go
@@ -2,13 +2,23 @@ package db
import (
"fmt"
- "github.com/mongodb/mongo-tools/common/bsonutil"
+ "strings"
+
"github.com/mongodb/mongo-tools/common/log"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
- "strings"
)
+type CollectionInfo struct {
+ Name string `bson:"name"`
+ Type string `bson:"type"`
+ Options *bson.D `bson:"options"`
+}
+
+func (ci *CollectionInfo) IsView() bool {
+ return ci.Type == "view"
+}
+
// IsNoCmd reeturns true if err indicates a query command is not supported,
// otherwise, returns false.
func IsNoCmd(err error) bool {
@@ -16,11 +26,11 @@ func IsNoCmd(err error) bool {
return ok && strings.HasPrefix(e.Message, "no such cmd:")
}
-// IsNoCollection returns true if err indicates a query resulted in a "no collection" error
-// otherwise, returns false.
-func IsNoCollection(err error) bool {
+// IsNoNamespace returns true if err indicates a query resulted in a
+// "NamespaceNotFound" error otherwise, returns false.
+func IsNoNamespace(err error) bool {
e, ok := err.(*mgo.QueryError)
- return ok && e.Message == "no collection"
+ return ok && e.Code == 26
}
// buildBsonArray takes a cursor iterator and returns an array of
@@ -67,7 +77,7 @@ func GetIndexes(coll *mgo.Collection) (*mgo.Iter, error) {
case IsNoCmd(err):
log.Logvf(log.DebugLow, "No support for listIndexes command, falling back to querying system.indexes")
return getIndexesPre28(coll)
- case IsNoCollection(err):
+ case IsNoNamespace(err):
return nil, nil
default:
return nil, fmt.Errorf("error running `listIndexes`. Collection: `%v` Err: %v", coll.FullName, err)
@@ -124,7 +134,7 @@ func getCollectionsPre28(database *mgo.Database, name string) (*mgo.Iter, error)
return iter, nil
}
-func GetCollectionOptions(coll *mgo.Collection) (*bson.D, error) {
+func GetCollectionInfo(coll *mgo.Collection) (*CollectionInfo, error) {
iter, useFullName, err := GetCollections(coll.Database, coll.Name)
if err != nil {
return nil, err
@@ -134,33 +144,32 @@ func GetCollectionOptions(coll *mgo.Collection) (*bson.D, error) {
if useFullName {
comparisonName = coll.FullName
}
- collInfo := &bson.D{}
+
+ collInfo := &CollectionInfo{}
for iter.Next(collInfo) {
- name, err := bsonutil.FindValueByKey("name", collInfo)
- if err != nil {
- collInfo = nil
- continue
- }
- if nameStr, ok := name.(string); ok {
- if nameStr == comparisonName {
- // we've found the collection we're looking for
- break
+ if collInfo.Name == comparisonName {
+ if useFullName {
+ collName, err := StripDBFromNamespace(collInfo.Name, coll.Database.Name)
+ if err != nil {
+ return nil, err
+ }
+ collInfo.Name = collName
}
- } else {
- collInfo = nil
- continue
+ break
}
}
+ if err := iter.Err(); err != nil {
+ return nil, err
+ }
+ return collInfo, nil
+}
- if collInfo != nil {
- optsInterface, _ := bsonutil.FindValueByKey("options", collInfo)
- if optsInterface != nil {
- optsD, ok := optsInterface.(bson.D)
- if !ok {
- return nil, fmt.Errorf("Cannot unmarshal collection options for collection %v.%v", coll.Database, coll.Name)
- }
- return &optsD, nil
- }
+func StripDBFromNamespace(namespace string, dbName string) (string, error) {
+ namespacePrefix := dbName + "."
+ // if the collection info came from querying system.indexes (2.6 or earlier) then the
+ // "name" we get includes the db name as well, so we must remove it
+ if strings.HasPrefix(namespace, namespacePrefix) {
+ return namespace[len(namespacePrefix):], nil
}
- return nil, iter.Err()
+ return "", fmt.Errorf("namespace '%v' format is invalid - expected to start with '%v'", namespace, namespacePrefix)
}
diff --git a/src/mongo/gotools/common/db/namespaces_test.go b/src/mongo/gotools/common/db/namespaces_test.go
new file mode 100644
index 00000000000..0e390a2f43c
--- /dev/null
+++ b/src/mongo/gotools/common/db/namespaces_test.go
@@ -0,0 +1,52 @@
+package db
+
+import (
+ "fmt"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+type stripDBFromNamespaceTestCase struct {
+ inputNamespace string
+ inputDBName string
+
+ outputNamespace string
+ outputError error
+}
+
+func TestStripDBFromNamespace(t *testing.T) {
+ Convey("When testing StripDBFromNamespace with cases", t, func() {
+ testCases := []stripDBFromNamespaceTestCase{
+ {
+ inputNamespace: "database.col",
+ inputDBName: "database",
+
+ outputNamespace: "col",
+ outputError: nil,
+ },
+ {
+ inputNamespace: "database2.col",
+ inputDBName: "database",
+
+ outputNamespace: "",
+ outputError: fmt.Errorf("namespace 'database2.col' format is invalid - expected to start with 'database.'"),
+ },
+ {
+ inputNamespace: "database.col",
+ inputDBName: "notAPrefix",
+
+ outputNamespace: "",
+ outputError: fmt.Errorf("namespace 'database.col' format is invalid - expected to start with 'notAPrefix.'"),
+ },
+ }
+ Convey("cases should match expected", func() {
+ for _, tc := range testCases {
+ resultNamespace, resultError := StripDBFromNamespace(tc.inputNamespace, tc.inputDBName)
+ So(resultError, ShouldResemble, tc.outputError)
+ So(resultNamespace, ShouldEqual, tc.outputNamespace)
+ }
+ })
+ })
+
+}
diff --git a/src/mongo/gotools/common/db/openssl/openssl.go b/src/mongo/gotools/common/db/openssl/openssl.go
index 1cc4c2ccd1f..db9d71c85aa 100644
--- a/src/mongo/gotools/common/db/openssl/openssl.go
+++ b/src/mongo/gotools/common/db/openssl/openssl.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/mongodb/mongo-tools/common/db/kerberos"
+ "github.com/mongodb/mongo-tools/common/log"
"github.com/mongodb/mongo-tools/common/options"
"github.com/mongodb/mongo-tools/common/util"
"github.com/spacemonkeygo/openssl"
@@ -15,17 +16,14 @@ import (
// For connecting to the database over ssl
type SSLDBConnector struct {
- dialInfo *mgo.DialInfo
- dialError error
- ctx *openssl.Ctx
+ dialInfo *mgo.DialInfo
+ ctx *openssl.Ctx
}
// Configure the connector to connect to the server over ssl. Parses the
// connection string, and sets up the correct function to dial the server
// based on the ssl options passed in.
func (self *SSLDBConnector) Configure(opts options.ToolOptions) error {
- // create the addresses to be used to connect
- connectionAddrs := util.CreateConnectionAddrs(opts.Host, opts.Port)
var err error
self.ctx, err = setupCtx(opts)
@@ -41,15 +39,26 @@ func (self *SSLDBConnector) Configure(opts options.ToolOptions) error {
// create the dialer func that will be used to connect
dialer := func(addr *mgo.ServerAddr) (net.Conn, error) {
conn, err := openssl.Dial("tcp", addr.String(), self.ctx, flags)
- self.dialError = err
- return conn, err
+ if err != nil {
+ // mgo discards dialer errors so log it now
+ log.Logvf(log.Always, "error dialing %v: %v", addr.String(), err)
+ return nil, err
+ }
+ // enable TCP keepalive
+ err = util.EnableTCPKeepAlive(conn.UnderlyingConn(), time.Duration(opts.TCPKeepAliveSeconds)*time.Second)
+ if err != nil {
+ // mgo discards dialer errors so log it now
+ log.Logvf(log.Always, "error enabling TCP keepalive on connection to %v: %v", addr.String(), err)
+ conn.Close()
+ return nil, err
+ }
+ return conn, nil
}
timeout := time.Duration(opts.Timeout) * time.Second
// set up the dial info
self.dialInfo = &mgo.DialInfo{
- Addrs: connectionAddrs,
Timeout: timeout,
Direct: opts.Direct,
ReplicaSetName: opts.ReplicaSetName,
@@ -59,6 +68,13 @@ func (self *SSLDBConnector) Configure(opts options.ToolOptions) error {
Source: opts.GetAuthenticationDatabase(),
Mechanism: opts.Auth.Mechanism,
}
+
+ // create or fetch the addresses to be used to connect
+ if opts.URI != nil && opts.URI.ConnectionString != "" {
+ self.dialInfo.Addrs = opts.URI.GetConnectionAddrs()
+ } else {
+ self.dialInfo.Addrs = util.CreateConnectionAddrs(opts.Host, opts.Port)
+ }
kerberos.AddKerberosOpts(opts, self.dialInfo)
return nil
@@ -66,11 +82,7 @@ func (self *SSLDBConnector) Configure(opts options.ToolOptions) error {
// Dial the server.
func (self *SSLDBConnector) GetNewSession() (*mgo.Session, error) {
- session, err := mgo.DialWithInfo(self.dialInfo)
- if err != nil && self.dialError != nil {
- return nil, fmt.Errorf("%v, openssl error: %v", err, self.dialError)
- }
- return session, err
+ return mgo.DialWithInfo(self.dialInfo)
}
// To be handed to mgo.DialInfo for connecting to the server.
diff --git a/src/mongo/gotools/common/db/write_concern.go b/src/mongo/gotools/common/db/write_concern.go
index 0a9a16214c8..8759fb59ad1 100644
--- a/src/mongo/gotools/common/db/write_concern.go
+++ b/src/mongo/gotools/common/db/write_concern.go
@@ -1,12 +1,15 @@
package db
import (
- "fmt"
+ "github.com/mongodb/mongo-tools/common/connstring"
"github.com/mongodb/mongo-tools/common/json"
"github.com/mongodb/mongo-tools/common/log"
"github.com/mongodb/mongo-tools/common/util"
"gopkg.in/mgo.v2"
+
+ "fmt"
"strconv"
+ "time"
)
// write concern fields
@@ -85,13 +88,60 @@ func constructWCObject(writeConcern string) (sessionSafety *mgo.Safe, err error)
return sessionSafety, nil
}
+// constructSafetyFromConnString takes in a parsed connection string and attempts
+// to construct an mgo.Safe object from it. It returns an error if it is unable
+// to parse the write concern value.
+func constructSafetyFromConnString(cs *connstring.ConnString) (*mgo.Safe, error) {
+ safe := &mgo.Safe{}
+
+ wValue, err := strconv.Atoi(cs.W)
+ if err != nil {
+ safe.WMode = cs.W
+ } else {
+ safe.W = wValue
+ if wValue < 0 {
+ return nil, fmt.Errorf("invalid '%v' argument: %v", w, wValue)
+ }
+ }
+
+ safe.WTimeout = int(cs.WTimeout / time.Second)
+ safe.FSync = cs.FSync
+ safe.J = cs.Journal
+
+ if safe.WMode == "" && safe.W == 0 && !safe.J {
+ return nil, nil
+ }
+
+ return safe, nil
+}
+
// BuildWriteConcern takes a string and a NodeType indicating the type of node the write concern
// is intended to be used against, and converts the write concern string argument into an
// mgo.Safe object that's usable on sessions for that node type.
-func BuildWriteConcern(writeConcern string, nodeType NodeType) (*mgo.Safe, error) {
- sessionSafety, err := constructWCObject(writeConcern)
- if err != nil {
- return nil, err
+func BuildWriteConcern(writeConcern string, nodeType NodeType, cs *connstring.ConnString) (*mgo.Safe, error) {
+ var sessionSafety *mgo.Safe
+ var err error
+
+ if cs != nil && writeConcern != "" {
+ return nil, fmt.Errorf("cannot specify writeConcern string and connectionString object")
+ }
+
+ if cs != nil {
+ if cs.W == "" {
+ cs.W = "majority"
+ }
+ sessionSafety, err = constructSafetyFromConnString(cs)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ if writeConcern == "" {
+ writeConcern = "majority"
+ }
+ sessionSafety, err = constructWCObject(writeConcern)
+ if err != nil {
+ return nil, err
+ }
}
if sessionSafety == nil {
diff --git a/src/mongo/gotools/common/db/write_concern_test.go b/src/mongo/gotools/common/db/write_concern_test.go
index 96bd8e0ed89..31a5baf1bfb 100644
--- a/src/mongo/gotools/common/db/write_concern_test.go
+++ b/src/mongo/gotools/common/db/write_concern_test.go
@@ -1,53 +1,94 @@
package db
import (
+ "github.com/mongodb/mongo-tools/common/connstring"
. "github.com/smartystreets/goconvey/convey"
+
+ "fmt"
"testing"
+ "time"
)
func TestBuildWriteConcern(t *testing.T) {
- Convey("Given a write concern string value, and a boolean indicating if the "+
- "write concern is to be used on a replica set, on calling BuildWriteConcern...", t, func() {
- Convey("no error should be returned if the write concern is valid", func() {
- writeConcern, err := BuildWriteConcern(`{w:34}`, ReplSet)
- So(err, ShouldBeNil)
- So(writeConcern.W, ShouldEqual, 34)
- writeConcern, err = BuildWriteConcern(`{w:"majority"}`, ReplSet)
- So(err, ShouldBeNil)
- So(writeConcern.WMode, ShouldEqual, "majority")
- writeConcern, err = BuildWriteConcern(`majority`, ReplSet)
- So(err, ShouldBeNil)
- So(writeConcern.WMode, ShouldEqual, "majority")
- writeConcern, err = BuildWriteConcern(`tagset`, ReplSet)
- So(err, ShouldBeNil)
- So(writeConcern.WMode, ShouldEqual, "tagset")
+ Convey("When building write concern object", t, func() {
+ Convey("and given a write concern string value, and a boolean indicating if the "+
+ "write concern is to be used on a replica set, on calling BuildWriteConcern...", func() {
+ Convey("no error should be returned if the write concern is valid", func() {
+ writeConcern, err := BuildWriteConcern(`{w:34}`, ReplSet, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 34)
+ writeConcern, err = BuildWriteConcern(`{w:"majority"}`, ReplSet, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.WMode, ShouldEqual, "majority")
+ writeConcern, err = BuildWriteConcern(`majority`, ReplSet, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.WMode, ShouldEqual, "majority")
+ writeConcern, err = BuildWriteConcern(`tagset`, ReplSet, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.WMode, ShouldEqual, "tagset")
+ })
+ Convey("on replica sets, only a write concern of 1 or 0 should be returned", func() {
+ writeConcern, err := BuildWriteConcern(`{w:34}`, Standalone, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 1)
+ writeConcern, err = BuildWriteConcern(`{w:"majority"}`, Standalone, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 1)
+ writeConcern, err = BuildWriteConcern(`tagset`, Standalone, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 1)
+ })
+ Convey("with a w value of 0, without j set, a nil write concern should be returned", func() {
+ writeConcern, err := BuildWriteConcern(`{w:0}`, Standalone, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern, ShouldBeNil)
+ })
+ Convey("with a negative w value, an error should be returned", func() {
+ _, err := BuildWriteConcern(`{w:-1}`, ReplSet, nil)
+ So(err, ShouldNotBeNil)
+ _, err = BuildWriteConcern(`{w:-2}`, ReplSet, nil)
+ So(err, ShouldNotBeNil)
+ })
+ Convey("with a w value of 0, with j set, a non-nil write concern should be returned", func() {
+ writeConcern, err := BuildWriteConcern(`{w:0, j:true}`, Standalone, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.J, ShouldBeTrue)
+ })
+ // Regression test for TOOLS-1741
+ Convey("When passing an empty writeConcern and empty URI"+
+ "then write concern should default to being majority", func() {
+ writeConcern, err := BuildWriteConcern("", ReplSet, nil)
+ So(err, ShouldBeNil)
+ So(writeConcern.WMode, ShouldEqual, "majority")
+ })
})
- Convey("on replica sets, only a write concern of 1 or 0 should be returned", func() {
- writeConcern, err := BuildWriteConcern(`{w:34}`, Standalone)
- So(err, ShouldBeNil)
- So(writeConcern.W, ShouldEqual, 1)
- writeConcern, err = BuildWriteConcern(`{w:"majority"}`, Standalone)
- So(err, ShouldBeNil)
- So(writeConcern.W, ShouldEqual, 1)
- writeConcern, err = BuildWriteConcern(`tagset`, Standalone)
- So(err, ShouldBeNil)
- So(writeConcern.W, ShouldEqual, 1)
- })
- Convey("with a w value of 0, without j set, a nil write concern should be returned", func() {
- writeConcern, err := BuildWriteConcern(`{w:0}`, Standalone)
- So(err, ShouldBeNil)
- So(writeConcern, ShouldBeNil)
+ Convey("and given a connection string", func() {
+ Convey("with a w value of 0, without j set, a nil write concern should be returned", func() {
+ writeConcern, err := BuildWriteConcern(``, Standalone, &connstring.ConnString{W: "0"})
+ So(err, ShouldBeNil)
+ So(writeConcern, ShouldBeNil)
+ })
+ Convey("with a negative w value, an error should be returned", func() {
+ _, err := BuildWriteConcern(``, ReplSet, &connstring.ConnString{W: "-1"})
+ So(err, ShouldNotBeNil)
+ _, err = BuildWriteConcern(``, ReplSet, &connstring.ConnString{W: "-2"})
+ So(err, ShouldNotBeNil)
+ })
+ Convey("on replica sets, only a write concern of 1 or 0 should be returned", func() {
+ writeConcern, err := BuildWriteConcern(``, Standalone, &connstring.ConnString{W: "34"})
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 1)
+ writeConcern, err = BuildWriteConcern(``, Standalone, &connstring.ConnString{W: "majority"})
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 1)
+ writeConcern, err = BuildWriteConcern(``, Standalone, &connstring.ConnString{W: "tagset"})
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 1)
+ })
})
- Convey("with a negative w value, an error should be returned", func() {
- _, err := BuildWriteConcern(`{w:-1}`, ReplSet)
+ Convey("and given both, should error", func() {
+ _, err := BuildWriteConcern(`ab`, ReplSet, &connstring.ConnString{W: "-1"})
So(err, ShouldNotBeNil)
- _, err = BuildWriteConcern(`{w:-2}`, ReplSet)
- So(err, ShouldNotBeNil)
- })
- Convey("with a w value of 0, with j set, a non-nil write concern should be returned", func() {
- writeConcern, err := BuildWriteConcern(`{w:0, j:true}`, Standalone)
- So(err, ShouldBeNil)
- So(writeConcern.J, ShouldBeTrue)
})
})
}
@@ -164,3 +205,57 @@ func TestConstructWCObject(t *testing.T) {
})
})
}
+
+func TestConstructSafetyFromConnString(t *testing.T) {
+ Convey("Given a parsed &connstring, on calling constructSafetyFromConnString...", t, func() {
+
+ Convey("non string values should be assigned to the 'WMode' "+
+ "field in their entirety", func() {
+ writeConcernString := "majority"
+ cs := &connstring.ConnString{
+ W: writeConcernString,
+ }
+ writeConcern, err := constructSafetyFromConnString(cs)
+ So(err, ShouldBeNil)
+ So(writeConcern.WMode, ShouldEqual, writeConcernString)
+ })
+
+ Convey("Int values should be assigned to the 'w' field ", func() {
+ cs := &connstring.ConnString{
+ W: "4",
+ }
+ writeConcern, err := constructSafetyFromConnString(cs)
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, 4)
+ })
+
+ Convey("&connstrings with valid j, wtimeout, fsync and w, should be "+
+ "assigned accordingly", func() {
+ expectedW := 3
+ expectedWTimeout := 43
+ cs := &connstring.ConnString{
+ W: "3",
+ Journal: true,
+ FSync: false,
+ WTimeout: time.Second * 43,
+ }
+ writeConcern, err := constructSafetyFromConnString(cs)
+ So(err, ShouldBeNil)
+ So(writeConcern.W, ShouldEqual, expectedW)
+ So(writeConcern.J, ShouldBeTrue)
+ So(writeConcern.FSync, ShouldBeFalse)
+ So(writeConcern.WTimeout, ShouldEqual, expectedWTimeout)
+ })
+
+ Convey("Unacknowledge write concern strings should return a nil object "+
+ "if journaling is not required", func() {
+ cs := &connstring.ConnString{
+ W: "0",
+ }
+ writeConcern, err := constructSafetyFromConnString(cs)
+ fmt.Println(writeConcern)
+ So(err, ShouldBeNil)
+ So(writeConcern, ShouldBeNil)
+ })
+ })
+}
diff --git a/src/mongo/gotools/common/intents/intent.go b/src/mongo/gotools/common/intents/intent.go
index 8f317a3716f..db7eb78a772 100644
--- a/src/mongo/gotools/common/intents/intent.go
+++ b/src/mongo/gotools/common/intents/intent.go
@@ -285,6 +285,7 @@ func (manager *Manager) putNormalIntentWithNamespace(ns string, intent *Intent)
// Put inserts an intent into the manager with the same source namespace as
// its destinations.
func (manager *Manager) Put(intent *Intent) {
+ log.Logvf(log.DebugLow, "enqueued collection '%v'", intent.Namespace())
manager.PutWithNamespace(intent.Namespace(), intent)
}
diff --git a/src/mongo/gotools/common/intents/intent_prioritizer.go b/src/mongo/gotools/common/intents/intent_prioritizer.go
index 290a7c83d1e..bea15bb1ab9 100644
--- a/src/mongo/gotools/common/intents/intent_prioritizer.go
+++ b/src/mongo/gotools/common/intents/intent_prioritizer.go
@@ -59,8 +59,8 @@ func (legacy *legacyPrioritizer) Finish(*Intent) {
//===== Longest Task First =====
// longestTaskFirstPrioritizer returns intents in the order of largest -> smallest,
-// which is better at minimizing total runtime in parallel environments than
-// other simple orderings.
+// with views at the front of the list, which is better at minimizing total
+// runtime in parallel environments than other simple orderings.
type longestTaskFirstPrioritizer struct {
sync.Mutex
queue []*Intent
@@ -68,7 +68,7 @@ type longestTaskFirstPrioritizer struct {
// NewLongestTaskFirstPrioritizer returns an initialized LTP prioritizer
func NewLongestTaskFirstPrioritizer(intents []*Intent) *longestTaskFirstPrioritizer {
- sort.Sort(BySize(intents))
+ sort.Sort(BySizeAndView(intents))
return &longestTaskFirstPrioritizer{
queue: intents,
}
@@ -92,6 +92,22 @@ func (ltf *longestTaskFirstPrioritizer) Finish(*Intent) {
return
}
+// BySizeAndView attaches the methods for sort.Interface for sorting intents
+// from largest to smallest size, taking into account if it's a view or not.
+type BySizeAndView []*Intent
+
+func (s BySizeAndView) Len() int { return len(s) }
+func (s BySizeAndView) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s BySizeAndView) Less(i, j int) bool {
+ if s[i].IsView() && !s[j].IsView() {
+ return true
+ }
+ if !s[i].IsView() && s[j].IsView() {
+ return false
+ }
+ return s[i].Size > s[j].Size
+}
+
// For sorting intents from largest to smallest size
type BySize []*Intent
diff --git a/src/mongo/gotools/common/intents/intent_prioritizer_test.go b/src/mongo/gotools/common/intents/intent_prioritizer_test.go
index 2abd79e5641..2e153d3f9ea 100644
--- a/src/mongo/gotools/common/intents/intent_prioritizer_test.go
+++ b/src/mongo/gotools/common/intents/intent_prioritizer_test.go
@@ -2,9 +2,11 @@ package intents
import (
"container/heap"
+ "testing"
+
"github.com/mongodb/mongo-tools/common/testutil"
. "github.com/smartystreets/goconvey/convey"
- "testing"
+ "gopkg.in/mgo.v2/bson"
)
func TestLegacyPrioritizer(t *testing.T) {
@@ -109,6 +111,37 @@ func TestDBCounterCollectionSorting(t *testing.T) {
})
}
+func TestBySizeAndView(t *testing.T) {
+ var prioritizer IntentPrioritizer
+
+ testutil.VerifyTestType(t, testutil.UnitTestType)
+
+ Convey("With a prioritizer initialized with on a set of intents", t, func() {
+ intents := []*Intent{
+ &Intent{C: "non-view2", Size: 32},
+ &Intent{C: "view", Size: 0,
+ Options: &bson.D{{"viewOn", true}},
+ },
+ &Intent{C: "non-view1", Size: 1024},
+ &Intent{C: "non-view3", Size: 2},
+ &Intent{C: "view", Size: 0,
+ Options: &bson.D{{"viewOn", true}},
+ },
+ }
+ prioritizer = NewLongestTaskFirstPrioritizer(intents)
+ Convey("getting the sorted intents should produce views first, followed by largest to smallest", func() {
+
+ So(prioritizer.Get().C, ShouldEqual, "view")
+ So(prioritizer.Get().C, ShouldEqual, "view")
+ So(prioritizer.Get().C, ShouldEqual, "non-view1")
+ So(prioritizer.Get().C, ShouldEqual, "non-view2")
+ So(prioritizer.Get().C, ShouldEqual, "non-view3")
+ })
+
+ })
+
+}
+
func TestSimulatedMultiDBJob(t *testing.T) {
var prioritizer IntentPrioritizer
diff --git a/src/mongo/gotools/common/options/options.go b/src/mongo/gotools/common/options/options.go
index 5e52c733a7f..c33aab229ba 100644
--- a/src/mongo/gotools/common/options/options.go
+++ b/src/mongo/gotools/common/options/options.go
@@ -3,16 +3,18 @@
package options
import (
+ "fmt"
"github.com/jessevdk/go-flags"
+ "github.com/mongodb/mongo-tools/common/connstring"
"github.com/mongodb/mongo-tools/common/failpoint"
"github.com/mongodb/mongo-tools/common/log"
-
- "fmt"
+ "github.com/mongodb/mongo-tools/common/util"
"os"
"regexp"
"runtime"
"strconv"
"strings"
+ "time"
)
// Gitspec that the tool was built with. Needs to be set using -ldflags
@@ -21,6 +23,23 @@ var (
Gitspec = "built-without-git-spec"
)
+var (
+ KnownURIOptionsAuth = []string{"authsource", "authmechanism"}
+ KnownURIOptionsConnection = []string{"connecttimeoutms"}
+ KnownURIOptionsSSL = []string{"ssl"}
+ KnownURIOptionsReadPreference = []string{"readpreference"}
+ KnownURIOptionsKerberos = []string{"gssapiservicename", "gssapihostname"}
+ KnownURIOptionsWriteConcern = []string{"wtimeout", "w", "j", "fsync"}
+ KnownURIOptionsReplicaSet = []string{"replicaset"}
+)
+
+var (
+ BuiltWithSSL bool
+ BuiltWithGSSAPI bool
+)
+
+const IncompatibleArgsErrorFormat = "illegal argument combination: cannot specify %s and --uri"
+
// Struct encompassing all of the options that are reused across tools: "help",
// "version", verbosity settings, ssl settings, etc.
type ToolOptions struct {
@@ -32,6 +51,7 @@ type ToolOptions struct {
VersionStr string
// Sub-option types
+ *URI
*General
*Verbosity
*Connection
@@ -52,6 +72,9 @@ type ToolOptions struct {
// for caching the parser
parser *flags.Parser
+
+ // for checking which options were enabled on this tool
+ enabledOptions EnabledOptions
}
type Namespace struct {
@@ -65,7 +88,7 @@ type General struct {
Help bool `long:"help" description:"print usage"`
Version bool `long:"version" description:"print the tool version and exit"`
- MaxProcs int `long:"numThreads" default:"0" hidden:"true"`
+ MaxProcs int `long:"numThreads" hidden:"true"`
Failpoints string `long:"failpoints" hidden:"true"`
}
@@ -84,12 +107,21 @@ func (v Verbosity) IsQuiet() bool {
return v.Quiet
}
+type URI struct {
+ ConnectionString string `long:"uri" value-name:"mongodb-uri" description:"mongodb uri connection string"`
+
+ knownURIParameters []string
+ extraOptionsRegistry []ExtraOptions
+ connString connstring.ConnString
+}
+
// Struct holding connection-related options
type Connection struct {
Host string `short:"h" long:"host" value-name:"<hostname>" description:"mongodb host to connect to (setname/host1,host2 for replica sets)"`
Port string `long:"port" value-name:"<port>" description:"server port (can also use --host hostname:port)"`
- Timeout int `long:"dialTimeout" default:"3" hidden:"true" description:"dial timeout in seconds"`
+ Timeout int `long:"dialTimeout" default:"3" hidden:"true" description:"dial timeout in seconds"`
+ TCPKeepAliveSeconds int `long:"TCPKeepAliveSeconds" default:"30" hidden:"true" description:"seconds between TCP keep alives"`
}
// Struct holding ssl-related options
@@ -117,6 +149,16 @@ type Kerberos struct {
Service string `long:"gssapiServiceName" value-name:"<service-name>" description:"service name to use when authenticating using GSSAPI/Kerberos ('mongodb' by default)"`
ServiceHost string `long:"gssapiHostName" value-name:"<host-name>" description:"hostname to use when authenticating using GSSAPI/Kerberos (remote server's address by default)"`
}
+type WriteConcern struct {
+ // Specifies the write concern for each write operation that mongofiles writes to the target database.
+ // By default, mongofiles waits for a majority of members from the replica set to respond before returning.
+ WriteConcern string `long:"writeConcern" value-name:"<write-concern>" default:"majority" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}' (defaults to 'majority')"`
+
+ w int
+ wtimeout int
+ fsync bool
+ journal bool
+}
type OptionRegistrationFunction func(o *ToolOptions) error
@@ -126,6 +168,7 @@ type EnabledOptions struct {
Auth bool
Connection bool
Namespace bool
+ URI bool
}
func parseVal(val string) int {
@@ -146,12 +189,14 @@ func New(appName, usageStr string, enabled EnabledOptions) *ToolOptions {
General: &General{},
Verbosity: &Verbosity{},
Connection: &Connection{},
+ URI: &URI{},
SSL: &SSL{},
Auth: &Auth{},
Namespace: &Namespace{},
Kerberos: &Kerberos{},
parser: flags.NewNamedParser(
fmt.Sprintf("%v %v", appName, usageStr), flags.None),
+ enabledOptions: enabled,
}
// Called when -v or --verbose is parsed
@@ -172,6 +217,8 @@ func New(appName, usageStr string, enabled EnabledOptions) *ToolOptions {
opts.parser.UnknownOptionHandler = opts.handleUnknownOption
+ opts.URI.AddKnownURIParameters(KnownURIOptionsReplicaSet)
+
if _, err := opts.parser.AddGroup("general options", "", opts.General); err != nil {
panic(fmt.Errorf("couldn't register general options: %v", err))
}
@@ -183,6 +230,7 @@ func New(appName, usageStr string, enabled EnabledOptions) *ToolOptions {
EnableFailpoints(opts)
if enabled.Connection {
+ opts.URI.AddKnownURIParameters(KnownURIOptionsConnection)
if _, err := opts.parser.AddGroup("connection options", "", opts.Connection); err != nil {
panic(fmt.Errorf("couldn't register connection options: %v", err))
}
@@ -196,6 +244,7 @@ func New(appName, usageStr string, enabled EnabledOptions) *ToolOptions {
}
if enabled.Auth {
+ opts.URI.AddKnownURIParameters(KnownURIOptionsAuth)
if _, err := opts.parser.AddGroup("authentication options", "", opts.Auth); err != nil {
panic(fmt.Errorf("couldn't register auth options"))
}
@@ -205,7 +254,11 @@ func New(appName, usageStr string, enabled EnabledOptions) *ToolOptions {
panic(fmt.Errorf("couldn't register namespace options"))
}
}
-
+ if enabled.URI {
+ if _, err := opts.parser.AddGroup("uri options", "", opts.URI); err != nil {
+ panic(fmt.Errorf("couldn't register URI options"))
+ }
+ }
if opts.MaxProcs <= 0 {
opts.MaxProcs = runtime.NumCPU()
}
@@ -266,6 +319,13 @@ type ExtraOptions interface {
Name() string
}
+type URISetter interface {
+ // SetOptionsFromURI provides a way for tools to fetch any options that were
+ // set in the URI and set them on the ExtraOptions that they pass to the options
+ // package.
+ SetOptionsFromURI(connstring.ConnString) error
+}
+
func (auth *Auth) RequiresExternalDB() bool {
return auth.Mechanism == "GSSAPI" || auth.Mechanism == "PLAIN" || auth.Mechanism == "MONGODB-X509"
}
@@ -277,6 +337,52 @@ func (auth *Auth) ShouldAskForPassword() bool {
!(auth.Mechanism == "MONGODB-X509" || auth.Mechanism == "GSSAPI")
}
+func (uri *URI) GetConnectionAddrs() []string {
+ return uri.connString.Hosts
+}
+func (uri *URI) ParsedConnString() *connstring.ConnString {
+ if uri.ConnectionString == "" {
+ return nil
+ }
+ return &uri.connString
+}
+func (uri *URI) AddKnownURIParameters(uriFieldNames []string) {
+ uri.knownURIParameters = append(uri.knownURIParameters, uriFieldNames...)
+}
+
+func (opts *ToolOptions) EnabledToolOptions() EnabledOptions {
+ return opts.enabledOptions
+}
+
+func (uri *URI) LogUnsupportedOptions() {
+ allOptionsFromURI := map[string]struct{}{}
+
+ for optName := range uri.connString.Options {
+ allOptionsFromURI[optName] = struct{}{}
+ }
+
+ for optName := range uri.connString.UnknownOptions {
+ allOptionsFromURI[optName] = struct{}{}
+ }
+
+ for _, optName := range uri.knownURIParameters {
+ if _, ok := allOptionsFromURI[optName]; ok {
+ delete(allOptionsFromURI, optName)
+ }
+ }
+
+ unsupportedOptions := make([]string, len(allOptionsFromURI))
+ optionIndex := 0
+ for optionName := range allOptionsFromURI {
+ unsupportedOptions[optionIndex] = optionName
+ optionIndex++
+ }
+
+ for _, optName := range unsupportedOptions {
+ log.Logvf(log.Always, "WARNING: ignoring unsupported URI parameter '%v'", optName)
+ }
+}
+
// Get the authentication database to use. Should be the value of
// --authenticationDatabase if it's provided, otherwise, the database that's
// specified in the tool's --db arg.
@@ -298,13 +404,39 @@ func (o *ToolOptions) AddOptions(opts ExtraOptions) {
panic(fmt.Sprintf("error setting command line options for %v: %v",
opts.Name(), err))
}
+
+ if o.enabledOptions.URI {
+ o.URI.extraOptionsRegistry = append(o.URI.extraOptionsRegistry, opts)
+ }
}
// Parse the command line args. Returns any extra args not accounted for by
// parsing, as well as an error if the parsing returns an error.
-func (o *ToolOptions) Parse() ([]string, error) {
- args, err := o.parser.Parse()
+func (o *ToolOptions) ParseArgs(args []string) ([]string, error) {
+ args, err := o.parser.ParseArgs(args)
+ if err != nil {
+ return []string{}, err
+ }
+
+ // connect directly, unless a replica set name is explicitly specified
+ if o.Host != "" {
+ _, o.ReplicaSetName = util.ParseConnectionString(o.Host)
+ o.Direct = (o.ReplicaSetName == "")
+ }
+
failpoint.ParseFailpoints(o.Failpoints)
+
+ if o.URI != nil && o.URI.ConnectionString != "" {
+ cs, err := connstring.ParseURIConnectionString(o.URI.ConnectionString)
+ if err != nil {
+ return []string{}, err
+ }
+ err = o.setOptionsFromURI(cs)
+ if err != nil {
+ return []string{}, err
+ }
+ }
+
return args, err
}
@@ -317,6 +449,76 @@ func (opts *ToolOptions) handleUnknownOption(option string, arg flags.SplitArgum
return args, fmt.Errorf(`unknown option "%v"`, option)
}
+func (opts *ToolOptions) setOptionsFromURI(cs connstring.ConnString) error {
+ opts.URI.connString = cs
+
+ // if Connection settings are enabled, then verify that other methods
+ // of specifying weren't used and set timeout
+ if opts.enabledOptions.Connection {
+ switch {
+ case opts.Connection.Host != "":
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--host")
+ case opts.Connection.Port != "":
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--port")
+ case opts.Connection.Timeout != 3:
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--dialTimeout")
+ }
+ opts.Connection.Timeout = int(cs.ConnectTimeout / time.Millisecond)
+ }
+
+ if opts.enabledOptions.Auth {
+ switch {
+ case opts.Username != "":
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--username")
+ case opts.Password != "" && cs.Password != "":
+ return fmt.Errorf(IncompatibleArgsErrorFormat,
+ "illegal argument combination: cannot specify password in uri and --password")
+ case opts.Source != "":
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--authenticationDatabase")
+ case opts.Auth.Mechanism != "":
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--authenticationMechanism")
+ }
+ opts.Username = cs.Username
+ opts.Password = cs.Password
+ opts.Source = cs.AuthSource
+ opts.Auth.Mechanism = cs.AuthMechanism
+ }
+ if opts.enabledOptions.Namespace {
+ if opts.Namespace != nil && opts.Namespace.DB != "" {
+ return fmt.Errorf(IncompatibleArgsErrorFormat, "--db")
+ }
+ }
+
+ opts.Namespace.DB = cs.Database
+ opts.Direct = (cs.Connect == connstring.SingleConnect)
+ opts.ReplicaSetName = cs.ReplicaSet
+
+ if cs.UseSSL && !BuiltWithSSL {
+ return fmt.Errorf("cannot use ssl: tool not built with SSL support")
+ }
+ opts.SSL.UseSSL = cs.UseSSL
+
+ if cs.KerberosService != "" && !BuiltWithGSSAPI {
+ return fmt.Errorf("cannot specify gssapiservicename: tool not built with kerberos support")
+ }
+ if cs.KerberosServiceHost != "" && !BuiltWithGSSAPI {
+ return fmt.Errorf("cannot specify gssapihostname: tool not built with kerberos support")
+ }
+
+ opts.Kerberos.Service = cs.KerberosService
+ opts.Kerberos.ServiceHost = cs.KerberosServiceHost
+
+ for _, extraOpts := range opts.URI.extraOptionsRegistry {
+ if uriSetter, ok := extraOpts.(URISetter); ok {
+ err := uriSetter.SetOptionsFromURI(cs)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
// getIntArg returns 3 args: the parsed int value, a bool set to true if a value
// was consumed from the incoming args array during parsing, and an error
// value if parsing failed
diff --git a/src/mongo/gotools/common/options/options_gssapi.go b/src/mongo/gotools/common/options/options_gssapi.go
index f10aa3a2b3b..93d3e9fa930 100644
--- a/src/mongo/gotools/common/options/options_gssapi.go
+++ b/src/mongo/gotools/common/options/options_gssapi.go
@@ -8,5 +8,10 @@ func init() {
func registerGSSAPIOptions(self *ToolOptions) error {
_, err := self.parser.AddGroup("kerberos options", "", self.Kerberos)
- return err
+ if err != nil {
+ return err
+ }
+ self.URI.AddKnownURIParameters(KnownURIOptionsKerberos)
+ BuiltWithGSSAPI = true
+ return nil
}
diff --git a/src/mongo/gotools/common/options/options_ssl.go b/src/mongo/gotools/common/options/options_ssl.go
index e8b8f27171f..6fcd4da13b3 100644
--- a/src/mongo/gotools/common/options/options_ssl.go
+++ b/src/mongo/gotools/common/options/options_ssl.go
@@ -14,5 +14,12 @@ func init() {
func registerSSLOptions(self *ToolOptions) error {
_, err := self.parser.AddGroup("ssl options", "", self.SSL)
- return err
+ if err != nil {
+ return err
+ }
+ if self.enabledOptions.URI {
+ self.URI.AddKnownURIParameters(KnownURIOptionsSSL)
+ }
+ BuiltWithSSL = true
+ return nil
}
diff --git a/src/mongo/gotools/common/options/options_test.go b/src/mongo/gotools/common/options/options_test.go
index cc18af21506..39579ea36c5 100644
--- a/src/mongo/gotools/common/options/options_test.go
+++ b/src/mongo/gotools/common/options/options_test.go
@@ -1,13 +1,17 @@
package options
import (
+ "github.com/mongodb/mongo-tools/common/connstring"
. "github.com/smartystreets/goconvey/convey"
+
+ "runtime"
"testing"
+ "time"
)
func TestVerbosityFlag(t *testing.T) {
Convey("With a new ToolOptions", t, func() {
- enabled := EnabledOptions{false, false, false}
+ enabled := EnabledOptions{false, false, false, false}
optPtr := New("", "", enabled)
So(optPtr, ShouldNotBeNil)
So(optPtr.parser, ShouldNotBeNil)
@@ -73,3 +77,303 @@ func TestVerbosityFlag(t *testing.T) {
})
})
}
+
+type uriTester struct {
+ Name string
+ CS connstring.ConnString
+ OptsIn *ToolOptions
+ OptsExpected *ToolOptions
+ WithSSL bool
+ WithGSSAPI bool
+ ShouldError bool
+ ShouldAskForPassword bool
+}
+
+func TestParseAndSetOptions(t *testing.T) {
+ Convey("With a matrix of URIs and expected results", t, func() {
+ enabledURIOnly := EnabledOptions{false, false, false, true}
+ testCases := []uriTester{
+ {
+ Name: "not built with ssl",
+ CS: connstring.ConnString{
+ UseSSL: true,
+ },
+ WithSSL: false,
+ OptsIn: New("", "", enabledURIOnly),
+ OptsExpected: New("", "", enabledURIOnly),
+ ShouldError: true,
+ },
+ {
+ Name: "built with ssl",
+ CS: connstring.ConnString{
+ UseSSL: true,
+ },
+ WithSSL: true, OptsIn: New("", "", enabledURIOnly),
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{
+ UseSSL: true,
+ },
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: enabledURIOnly,
+ },
+ ShouldError: false,
+ },
+ {
+ Name: "not built with gssapi",
+ CS: connstring.ConnString{
+ KerberosService: "service",
+ },
+ WithGSSAPI: false,
+ OptsIn: New("", "", enabledURIOnly),
+ OptsExpected: New("", "", enabledURIOnly),
+ ShouldError: true,
+ },
+ {
+ Name: "built with gssapi",
+ CS: connstring.ConnString{
+ KerberosService: "service",
+ KerberosServiceHost: "servicehost",
+ },
+ WithGSSAPI: true,
+ OptsIn: New("", "", enabledURIOnly),
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{
+ Service: "service",
+ ServiceHost: "servicehost",
+ },
+ enabledOptions: enabledURIOnly,
+ },
+ ShouldError: false,
+ },
+ {
+ Name: "connection fields set",
+ CS: connstring.ConnString{
+ ConnectTimeout: time.Duration(100) * time.Millisecond,
+ },
+ OptsIn: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{
+ Timeout: 3,
+ },
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Connection: true, URI: true},
+ },
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{
+ Timeout: 100,
+ },
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Connection: true, URI: true},
+ },
+ ShouldError: false,
+ },
+ {
+ Name: "auth fields set",
+ CS: connstring.ConnString{
+ AuthMechanism: "MONGODB-X509",
+ AuthSource: "authSource",
+ Username: "user",
+ Password: "password",
+ },
+ OptsIn: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Auth: true, URI: true},
+ },
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{
+ Username: "user",
+ Password: "password",
+ Source: "authSource",
+ Mechanism: "MONGODB-X509",
+ },
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Connection: true, URI: true},
+ },
+ ShouldError: false,
+ },
+ {
+ Name: "should ask for password",
+ CS: connstring.ConnString{
+ AuthMechanism: "MONGODB-X509",
+ AuthSource: "authSource",
+ Username: "user",
+ },
+ OptsIn: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Auth: true, URI: true},
+ },
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{
+ Username: "user",
+ Source: "authSource",
+ Mechanism: "MONGODB-X509",
+ },
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Connection: true, URI: true},
+ },
+ ShouldError: false,
+ ShouldAskForPassword: true,
+ },
+ {
+ Name: "single connect sets 'Direct'",
+ CS: connstring.ConnString{
+ Connect: connstring.SingleConnect,
+ },
+ OptsIn: New("", "", enabledURIOnly),
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Direct: true,
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{URI: true},
+ },
+ ShouldError: false,
+ },
+ {
+ Name: "ReplSetName is set when CS contains it",
+ CS: connstring.ConnString{
+ ReplicaSet: "replset",
+ },
+ OptsIn: New("", "", enabledURIOnly),
+ OptsExpected: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{},
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{URI: true},
+ ReplicaSetName: "replset",
+ },
+ ShouldError: false,
+ },
+ {
+ Name: "fail when uri and options set",
+ CS: connstring.ConnString{
+ Hosts: []string{"host"},
+ },
+ OptsIn: &ToolOptions{
+ General: &General{},
+ Verbosity: &Verbosity{},
+ Connection: &Connection{
+ Host: "host",
+ },
+ URI: &URI{},
+ SSL: &SSL{},
+ Auth: &Auth{},
+ Namespace: &Namespace{},
+ Kerberos: &Kerberos{},
+ enabledOptions: EnabledOptions{Connection: true, URI: true},
+ },
+ OptsExpected: New("", "", EnabledOptions{Connection: true, URI: true}),
+ ShouldError: true,
+ },
+ }
+
+ Convey("results should match expected", func() {
+ for _, testCase := range testCases {
+ t.Log("Test Case:", testCase.Name)
+
+ testCase.OptsIn.URI.ConnectionString = "mongodb://dummy"
+ testCase.OptsExpected.URI.ConnectionString = "mongodb://dummy"
+
+ BuiltWithSSL = testCase.WithSSL
+ BuiltWithGSSAPI = testCase.WithGSSAPI
+
+ testCase.OptsIn.URI.connString = testCase.CS
+
+ err := testCase.OptsIn.setOptionsFromURI(testCase.CS)
+
+ if testCase.ShouldError {
+ So(err, ShouldNotBeNil)
+ } else {
+ So(err, ShouldBeNil)
+ }
+
+ So(testCase.OptsIn.Connection.Timeout, ShouldResemble, testCase.OptsExpected.Connection.Timeout)
+ So(testCase.OptsIn.Username, ShouldResemble, testCase.OptsExpected.Username)
+ So(testCase.OptsIn.Password, ShouldResemble, testCase.OptsExpected.Password)
+ So(testCase.OptsIn.Source, ShouldResemble, testCase.OptsExpected.Source)
+ So(testCase.OptsIn.Auth.Mechanism, ShouldResemble, testCase.OptsExpected.Auth.Mechanism)
+ So(testCase.OptsIn.Direct, ShouldResemble, testCase.OptsExpected.Direct)
+ So(testCase.OptsIn.ReplicaSetName, ShouldResemble, testCase.OptsExpected.ReplicaSetName)
+ So(testCase.OptsIn.SSL.UseSSL, ShouldResemble, testCase.OptsExpected.SSL.UseSSL)
+ So(testCase.OptsIn.Kerberos.Service, ShouldResemble, testCase.OptsExpected.Kerberos.Service)
+ So(testCase.OptsIn.Kerberos.ServiceHost, ShouldResemble, testCase.OptsExpected.Kerberos.ServiceHost)
+ So(testCase.OptsIn.Auth.ShouldAskForPassword(), ShouldEqual, testCase.OptsIn.ShouldAskForPassword())
+ }
+ })
+ })
+}
+
+// Regression test for TOOLS-1694 to prevent issue from TOOLS-1115
+func TestHiddenOptionsDefaults(t *testing.T) {
+ Convey("With a ToolOptions parsed", t, func() {
+ enabled := EnabledOptions{Connection: true}
+ opts := New("", "", enabled)
+ _, err := opts.parser.ParseArgs([]string{})
+ So(err, ShouldBeNil)
+ Convey("hidden options should have expected values", func() {
+ So(opts.MaxProcs, ShouldEqual, runtime.NumCPU())
+ So(opts.Timeout, ShouldEqual, 3)
+ })
+ })
+
+}
diff --git a/src/mongo/gotools/common/util/net.go b/src/mongo/gotools/common/util/net.go
new file mode 100644
index 00000000000..1459d4abf95
--- /dev/null
+++ b/src/mongo/gotools/common/util/net.go
@@ -0,0 +1,24 @@
+package util
+
+import (
+ "net"
+ "time"
+)
+
+// EnableTCPKeepAlive enables TCP keepalive on the underlying TCP connection.
+func EnableTCPKeepAlive(conn net.Conn, keepAlivePeriod time.Duration) error {
+ if keepAlivePeriod == 0 {
+ return nil
+ }
+ if tcpconn, ok := conn.(*net.TCPConn); ok {
+ err := tcpconn.SetKeepAlive(true)
+ if err != nil {
+ return err
+ }
+ err = tcpconn.SetKeepAlivePeriod(keepAlivePeriod)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/src/mongo/gotools/import.data b/src/mongo/gotools/import.data
index bdf1375db42..081df338405 100644
--- a/src/mongo/gotools/import.data
+++ b/src/mongo/gotools/import.data
@@ -1,5 +1,6 @@
{
- "commit": "8bda55730d30c414a71dfbe6f45f5c54ef97811d",
+ "commit": "7067f765eb93ff5e1c601fc57317e68eda1978a5",
"github": "mongodb/mongo-tools.git",
+ "vendor": "tools",
"branch": "master"
}
diff --git a/src/mongo/gotools/mongodump/main/mongodump.go b/src/mongo/gotools/mongodump/main/mongodump.go
index 1845c4afdb1..9f6945b9e01 100644
--- a/src/mongo/gotools/mongodump/main/mongodump.go
+++ b/src/mongo/gotools/mongodump/main/mongodump.go
@@ -20,14 +20,15 @@ const (
func main() {
// initialize command-line opts
- opts := options.New("mongodump", mongodump.Usage, options.EnabledOptions{true, true, true})
+ opts := options.New("mongodump", mongodump.Usage, options.EnabledOptions{Auth: true, Connection: true, Namespace: true, URI: true})
inputOpts := &mongodump.InputOptions{}
opts.AddOptions(inputOpts)
outputOpts := &mongodump.OutputOptions{}
opts.AddOptions(outputOpts)
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsReadPreference)
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongodump --help' for more information")
@@ -53,10 +54,8 @@ func main() {
// init logger
log.SetVerbosity(opts.Verbosity)
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
// kick off the progress bar manager
progressManager := progress.NewBarWriter(log.Writer(0), progressBarWaitTime, progressBarLength, false)
diff --git a/src/mongo/gotools/mongodump/mongodump.go b/src/mongo/gotools/mongodump/mongodump.go
index f869d32673d..f7d5b48fb83 100644
--- a/src/mongo/gotools/mongodump/mongodump.go
+++ b/src/mongo/gotools/mongodump/mongodump.go
@@ -47,6 +47,7 @@ type MongoDump struct {
query bson.M
oplogCollection string
oplogStart bson.MongoTimestamp
+ oplogEnd bson.MongoTimestamp
isMongos bool
authVersion int
archive *archive.Writer
@@ -364,7 +365,7 @@ func (dump *MongoDump) Dump() (err error) {
return fmt.Errorf("error finding oplog: %v", err)
}
log.Logvf(log.Info, "getting most recent oplog timestamp")
- dump.oplogStart, err = dump.getOplogStartTime()
+ dump.oplogStart, err = dump.getCurrentOplogTime()
if err != nil {
return fmt.Errorf("error getting oplog start: %v", err)
}
@@ -396,6 +397,11 @@ func (dump *MongoDump) Dump() (err error) {
// we check to see if the oplog has rolled over (i.e. the most recent entry when
// we started still exist, so we know we haven't lost data)
if dump.OutputOptions.Oplog {
+ dump.oplogEnd, err = dump.getCurrentOplogTime()
+ if err != nil {
+ return fmt.Errorf("error getting oplog end: %v", err)
+ }
+
log.Logvf(log.DebugLow, "checking if oplog entry %v still exists", dump.oplogStart)
exists, err := dump.checkOplogTimestampExists(dump.oplogStart)
if !exists {
@@ -408,7 +414,8 @@ func (dump *MongoDump) Dump() (err error) {
log.Logvf(log.DebugHigh, "oplog entry %v still exists", dump.oplogStart)
log.Logvf(log.Always, "writing captured oplog to %v", dump.manager.Oplog().Location)
- err = dump.DumpOplogAfterTimestamp(dump.oplogStart)
+
+ err = dump.DumpOplogBetweenTimestamps(dump.oplogStart, dump.oplogEnd)
if err != nil {
return fmt.Errorf("error dumping oplog: %v", err)
}
diff --git a/src/mongo/gotools/mongodump/oplog_dump.go b/src/mongo/gotools/mongodump/oplog_dump.go
index ab7492bafd3..8e7c89c2588 100644
--- a/src/mongo/gotools/mongodump/oplog_dump.go
+++ b/src/mongo/gotools/mongodump/oplog_dump.go
@@ -35,8 +35,8 @@ func (dump *MongoDump) determineOplogCollectionName() error {
}
-// getOplogStartTime returns the most recent oplog entry
-func (dump *MongoDump) getOplogStartTime() (bson.MongoTimestamp, error) {
+// getOplogCurrentTime returns the most recent oplog entry
+func (dump *MongoDump) getCurrentOplogTime() (bson.MongoTimestamp, error) {
mostRecentOplogEntry := db.Oplog{}
err := dump.SessionProvider.FindOne("local", dump.oplogCollection, 0, nil, []string{"-$natural"}, &mostRecentOplogEntry, 0)
@@ -66,16 +66,19 @@ func (dump *MongoDump) checkOplogTimestampExists(ts bson.MongoTimestamp) (bool,
return true, nil
}
-// DumpOplogAfterTimestamp takes a timestamp and writer and dumps all oplog entries after
-// the given timestamp to the writer. Returns any errors that occur.
-func (dump *MongoDump) DumpOplogAfterTimestamp(ts bson.MongoTimestamp) error {
+// DumpOplogBetweenTimestamps takes two timestamps and writer and dumps all oplog
+// entries between the given timestamp to the writer. Returns any errors that occur.
+func (dump *MongoDump) DumpOplogBetweenTimestamps(start, end bson.MongoTimestamp) error {
session, err := dump.SessionProvider.GetSession()
if err != nil {
return err
}
defer session.Close()
session.SetPrefetch(1.0) // mimic exhaust cursor
- queryObj := bson.M{"ts": bson.M{"$gt": ts}}
+ queryObj := bson.M{"$and": []bson.M{
+ bson.M{"ts": bson.M{"$gte": start}},
+ bson.M{"ts": bson.M{"$lte": end}},
+ }}
oplogQuery := session.DB("local").C(dump.oplogCollection).Find(queryObj).LogReplay()
oplogCount, err := dump.dumpQueryToIntent(oplogQuery, dump.manager.Oplog(), dump.getResettableOutputBuffer())
if err == nil {
diff --git a/src/mongo/gotools/mongodump/options.go b/src/mongo/gotools/mongodump/options.go
index 9de83c3358f..80396119fe0 100644
--- a/src/mongo/gotools/mongodump/options.go
+++ b/src/mongo/gotools/mongodump/options.go
@@ -1,6 +1,9 @@
package mongodump
import (
+ "github.com/mongodb/mongo-tools/common/connstring"
+ "github.com/mongodb/mongo-tools/common/options"
+
"fmt"
"io/ioutil"
)
@@ -26,6 +29,14 @@ func (*InputOptions) Name() string {
return "query"
}
+func (inputOpts *InputOptions) SetOptionsFromURI(cs connstring.ConnString) error {
+ if inputOpts.ReadPreference != "" {
+ return fmt.Errorf(options.IncompatibleArgsErrorFormat, "--readPreference")
+ }
+ inputOpts.ReadPreference = cs.ReadPreference
+ return nil
+}
+
func (inputOptions *InputOptions) HasQuery() bool {
return inputOptions.Query != "" || inputOptions.QueryFile != ""
}
diff --git a/src/mongo/gotools/mongodump/prepare.go b/src/mongo/gotools/mongodump/prepare.go
index 81a258e73eb..75d7276e08f 100644
--- a/src/mongo/gotools/mongodump/prepare.go
+++ b/src/mongo/gotools/mongodump/prepare.go
@@ -12,7 +12,6 @@ import (
"github.com/mongodb/mongo-tools/common/db"
"github.com/mongodb/mongo-tools/common/intents"
"github.com/mongodb/mongo-tools/common/log"
- "gopkg.in/mgo.v2/bson"
)
type NilPos struct{}
@@ -21,16 +20,6 @@ func (NilPos) Pos() int64 {
return -1
}
-type collectionInfo struct {
- Name string `bson:"name"`
- Type string `bson:"type"`
- Options *bson.D `bson:"options"`
-}
-
-func (ci *collectionInfo) IsView() bool {
- return ci.Type == "view"
-}
-
// writeFlusher wraps an io.Writer and adds a Flush function.
type writeFlusher interface {
Flush() error
@@ -179,55 +168,6 @@ func checkStringForPathSeparator(s string, c *rune) bool {
return false
}
-// NewIntent creates a bare intent without populating the options.
-func (dump *MongoDump) NewIntent(dbName, colName string) (*intents.Intent, error) {
- intent := &intents.Intent{
- DB: dbName,
- C: colName,
- }
- if dump.OutputOptions.Out == "-" {
- intent.BSONFile = &stdoutFile{Writer: dump.OutputWriter}
- } else {
- if dump.OutputOptions.Archive != "" {
- intent.BSONFile = &archive.MuxIn{Intent: intent, Mux: dump.archive.Mux}
- } else {
- var c rune
- if checkStringForPathSeparator(colName, &c) || checkStringForPathSeparator(dbName, &c) {
- return nil, fmt.Errorf(`"%v.%v" contains a path separator '%c' `+
- `and can't be dumped to the filesystem`, dbName, colName, c)
- }
- path := nameGz(dump.OutputOptions.Gzip, dump.outputPath(dbName, colName)+".bson")
- intent.BSONFile = &realBSONFile{path: path, intent: intent}
- }
- if !intent.IsSystemIndexes() {
- if dump.OutputOptions.Archive != "" {
- intent.MetadataFile = &archive.MetadataFile{
- Intent: intent,
- Buffer: &bytes.Buffer{},
- }
- } else {
- path := nameGz(dump.OutputOptions.Gzip, dump.outputPath(dbName, colName+".metadata.json"))
- intent.MetadataFile = &realMetadataFile{path: path, intent: intent}
- }
- }
- }
-
- // get a document count for scheduling purposes
- session, err := dump.SessionProvider.GetSession()
- if err != nil {
- return nil, err
- }
- defer session.Close()
-
- count, err := session.DB(dbName).C(colName).Count()
- if err != nil {
- return nil, fmt.Errorf("error counting %v: %v", intent.Namespace(), err)
- }
- intent.Size = int64(count)
-
- return intent, nil
-}
-
// CreateOplogIntents creates an intents.Intent for the oplog and adds it to the manager
func (dump *MongoDump) CreateOplogIntents() error {
err := dump.determineOplogCollectionName()
@@ -290,57 +230,94 @@ func (dump *MongoDump) CreateCollectionIntent(dbName, colName string) error {
return nil
}
- intent, err := dump.NewIntent(dbName, colName)
- if err != nil {
- return err
- }
-
session, err := dump.SessionProvider.GetSession()
if err != nil {
return err
}
defer session.Close()
- intent.Options, err = db.GetCollectionOptions(session.DB(dbName).C(colName))
+ collOptions, err := db.GetCollectionInfo(session.DB(dbName).C(colName))
if err != nil {
return fmt.Errorf("error getting collection options: %v", err)
}
- dump.manager.Put(intent)
+ intent, err := dump.NewIntentFromOptions(dbName, collOptions)
+ if err != nil {
+ return err
+ }
- log.Logvf(log.DebugLow, "enqueued collection '%v'", intent.Namespace())
+ dump.manager.Put(intent)
return nil
}
-func (dump *MongoDump) createIntentFromOptions(dbName string, ci *collectionInfo) error {
- if dump.shouldSkipCollection(ci.Name) {
- log.Logvf(log.DebugLow, "skipping dump of %v.%v, it is excluded", dbName, ci.Name)
- return nil
+func (dump *MongoDump) NewIntentFromOptions(dbName string, ci *db.CollectionInfo) (*intents.Intent, error) {
+ intent := &intents.Intent{
+ DB: dbName,
+ C: ci.Name,
+ Options: ci.Options,
}
- if dump.OutputOptions.ViewsAsCollections && !ci.IsView() {
- log.Logvf(log.DebugLow, "skipping dump of %v.%v because it is not a view", dbName, ci.Name)
- return nil
+ // Setup output location
+ if dump.OutputOptions.Out == "-" { // regular standard output
+ intent.BSONFile = &stdoutFile{Writer: dump.OutputWriter}
+ } else {
+ // Set the BSONFile path.
+ if dump.OutputOptions.Archive != "" {
+ // if archive mode, then the output should be written using an output
+ // muxer.
+ intent.BSONFile = &archive.MuxIn{Intent: intent, Mux: dump.archive.Mux}
+ } else if dump.OutputOptions.ViewsAsCollections || !ci.IsView() {
+ // otherwise, if it's either not a view or we're treating views as collections
+ // then create a standard filesystem path for this collection.
+ var c rune
+ if checkStringForPathSeparator(ci.Name, &c) || checkStringForPathSeparator(dbName, &c) {
+ return nil, fmt.Errorf(`"%v.%v" contains a path separator '%c' `+
+ `and can't be dumped to the filesystem`, dbName, ci.Name, c)
+ }
+ path := nameGz(dump.OutputOptions.Gzip, dump.outputPath(dbName, ci.Name)+".bson")
+ intent.BSONFile = &realBSONFile{path: path, intent: intent}
+ } else {
+ // otherwise, it's a view and the options specify not dumping a view
+ // so don't dump it.
+ log.Logvf(log.DebugLow, "not dumping data for %v.%v because it is a view", dbName, ci.Name)
+ }
+ //Set the MetadataFile path.
+ if dump.OutputOptions.ViewsAsCollections && ci.IsView() {
+ log.Logvf(log.DebugLow, "not dumping metadata for %v.%v because it is a view", dbName, ci.Name)
+ } else {
+ if !intent.IsSystemIndexes() {
+ if dump.OutputOptions.Archive != "" {
+ intent.MetadataFile = &archive.MetadataFile{
+ Intent: intent,
+ Buffer: &bytes.Buffer{},
+ }
+ } else {
+ path := nameGz(dump.OutputOptions.Gzip, dump.outputPath(dbName, ci.Name+".metadata.json"))
+ intent.MetadataFile = &realMetadataFile{path: path, intent: intent}
+ }
+ }
+ }
}
- intent, err := dump.NewIntent(dbName, ci.Name)
+ // get a document count for scheduling purposes.
+ // skips this if it is a view, as it may be incredibly slow if the
+ // view is based on a slow query.
+
+ if ci.IsView() {
+ return intent, nil
+ }
+
+ session, err := dump.SessionProvider.GetSession()
if err != nil {
- return err
+ return nil, err
}
- if dump.OutputOptions.ViewsAsCollections {
- log.Logvf(log.DebugLow, "not dumping metadata for %v.%v because it is a view", dbName, ci.Name)
- intent.MetadataFile = nil
- } else if ci.IsView() {
- log.Logvf(log.DebugLow, "not dumping data for %v.%v because it is a view", dbName, ci.Name)
- // only write a bson file if using archive
- if dump.OutputOptions.Archive == "" {
- intent.BSONFile = nil
- }
+ defer session.Close()
+ count, err := session.DB(dbName).C(ci.Name).Count()
+ if err != nil {
+ return nil, fmt.Errorf("error counting %v: %v", intent.Namespace(), err)
}
- intent.Options = ci.Options
- dump.manager.Put(intent)
- log.Logvf(log.DebugLow, "enqueued collection '%v'", intent.Namespace())
- return nil
+ intent.Size = int64(count)
+ return intent, nil
}
// CreateIntentsForDatabase iterates through collections in a db
@@ -359,7 +336,7 @@ func (dump *MongoDump) CreateIntentsForDatabase(dbName string) error {
return fmt.Errorf("error getting collections for database `%v`: %v", dbName, err)
}
- collInfo := &collectionInfo{}
+ collInfo := &db.CollectionInfo{}
for colsIter.Next(collInfo) {
// ignore <db>.system.* except for admin
if dbName != "admin" && strings.HasPrefix(collInfo.Name, "system.") {
@@ -371,19 +348,26 @@ func (dump *MongoDump) CreateIntentsForDatabase(dbName string) error {
continue
}
if fullName {
- namespacePrefix := dbName + "."
- // if the collection info came from querying system.indexes (2.6 or earlier) then the
- // "name" we get includes the db name as well, so we must remove it
- if strings.HasPrefix(collInfo.Name, namespacePrefix) {
- collInfo.Name = collInfo.Name[len(namespacePrefix):]
- } else {
- return fmt.Errorf("namespace '%v' format is invalid - expected to start with '%v'", collInfo.Name, namespacePrefix)
+ collName, err := db.StripDBFromNamespace(collInfo.Name, dbName)
+ if err != nil {
+ return err
}
+ collInfo.Name = collName
+ }
+ if dump.shouldSkipCollection(collInfo.Name) {
+ log.Logvf(log.DebugLow, "skipping dump of %v.%v, it is excluded", dbName, collInfo.Name)
+ continue
+ }
+
+ if dump.OutputOptions.ViewsAsCollections && !collInfo.IsView() {
+ log.Logvf(log.DebugLow, "skipping dump of %v.%v because it is not a view", dbName, collInfo.Name)
+ continue
}
- err := dump.createIntentFromOptions(dbName, collInfo)
+ intent, err := dump.NewIntentFromOptions(dbName, collInfo)
if err != nil {
return err
}
+ dump.manager.Put(intent)
}
return colsIter.Err()
}
diff --git a/src/mongo/gotools/mongoexport/main/mongoexport.go b/src/mongo/gotools/mongoexport/main/mongoexport.go
index d7c7dba3e06..87407a3b246 100644
--- a/src/mongo/gotools/mongoexport/main/mongoexport.go
+++ b/src/mongo/gotools/mongoexport/main/mongoexport.go
@@ -24,14 +24,14 @@ const (
func main() {
// initialize command-line opts
opts := options.New("mongoexport", mongoexport.Usage,
- options.EnabledOptions{Auth: true, Connection: true, Namespace: true})
+ options.EnabledOptions{Auth: true, Connection: true, Namespace: true, URI: true})
outputOpts := &mongoexport.OutputFormatOptions{}
opts.AddOptions(outputOpts)
inputOpts := &mongoexport.InputOptions{}
opts.AddOptions(inputOpts)
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongoexport --help' for more information")
@@ -56,10 +56,8 @@ func main() {
return
}
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
provider, err := db.NewSessionProvider(*opts)
if err != nil {
diff --git a/src/mongo/gotools/mongoexport/mongoexport.go b/src/mongo/gotools/mongoexport/mongoexport.go
index 4feab4894d2..1508800790b 100644
--- a/src/mongo/gotools/mongoexport/mongoexport.go
+++ b/src/mongo/gotools/mongoexport/mongoexport.go
@@ -174,6 +174,7 @@ func makeFieldSelector(fields string) bson.M {
// It always returns Limit if there is a limit, assuming that in general
// limits will less then the total possible.
// If there is a query and no limit then it returns 0, because it's too expensive to count the query.
+// If the collection is a view then it returns 0, because it is too expensive to count the view.
// Otherwise it returns the count minus the skip
func (exp *MongoExport) getCount() (c int, err error) {
session, err := exp.SessionProvider.GetSession()
@@ -187,7 +188,17 @@ func (exp *MongoExport) getCount() (c int, err error) {
if exp.InputOpts != nil && exp.InputOpts.Query != "" {
return 0, nil
}
- q := session.DB(exp.ToolOptions.Namespace.DB).C(exp.ToolOptions.Namespace.Collection).Find(nil)
+ mgoCollection := session.DB(exp.ToolOptions.Namespace.DB).C(exp.ToolOptions.Namespace.Collection)
+
+ collInfo, err := db.GetCollectionInfo(mgoCollection)
+ if err != nil {
+ return 0, err
+ }
+ if collInfo.IsView() {
+ return 0, nil
+ }
+
+ q := mgoCollection.Find(nil)
c, err = q.Count()
if err != nil {
return 0, err
@@ -240,22 +251,15 @@ func (exp *MongoExport) getCursor() (*mgo.Iter, *mgo.Session, error) {
collection := session.DB(exp.ToolOptions.Namespace.DB).C(exp.ToolOptions.Namespace.Collection)
// figure out if we're exporting a view
- isView := false
- opts, err := db.GetCollectionOptions(collection)
+ collInfo, err := db.GetCollectionInfo(collection)
if err != nil {
return nil, nil, err
}
- if opts != nil {
- viewOn, _ := bsonutil.FindValueByKey("viewOn", opts)
- if viewOn != nil {
- isView = true
- }
- }
flags := 0
// don't snapshot if we've been asked not to,
// or if we cannot because we are querying, sorting, or if the collection is a view
- if !exp.InputOpts.ForceTableScan && len(query) == 0 && exp.InputOpts != nil && exp.InputOpts.Sort == "" && !isView {
+ if !exp.InputOpts.ForceTableScan && len(query) == 0 && exp.InputOpts != nil && exp.InputOpts.Sort == "" && !collInfo.IsView() {
flags = flags | db.Snapshot
}
diff --git a/src/mongo/gotools/mongofiles/main/mongofiles.go b/src/mongo/gotools/mongofiles/main/mongofiles.go
index 028dc940302..bff5748c8b3 100644
--- a/src/mongo/gotools/mongofiles/main/mongofiles.go
+++ b/src/mongo/gotools/mongofiles/main/mongofiles.go
@@ -2,26 +2,28 @@
package main
import (
- "fmt"
"github.com/mongodb/mongo-tools/common/db"
"github.com/mongodb/mongo-tools/common/log"
"github.com/mongodb/mongo-tools/common/options"
"github.com/mongodb/mongo-tools/common/signals"
"github.com/mongodb/mongo-tools/common/util"
"github.com/mongodb/mongo-tools/mongofiles"
+
+ "fmt"
"os"
)
func main() {
// initialize command-line opts
- opts := options.New("mongofiles", mongofiles.Usage, options.EnabledOptions{Auth: true, Connection: true, Namespace: false})
+ opts := options.New("mongofiles", mongofiles.Usage, options.EnabledOptions{Auth: true, Connection: true, Namespace: false, URI: true})
storageOpts := &mongofiles.StorageOptions{}
opts.AddOptions(storageOpts)
inputOpts := &mongofiles.InputOptions{}
opts.AddOptions(inputOpts)
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsReadPreference)
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongofiles --help' for more information")
@@ -40,14 +42,12 @@ func main() {
log.SetVerbosity(opts.Verbosity)
signals.Handle()
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
+
// add the specified database to the namespace options struct
opts.Namespace.DB = storageOpts.DB
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
-
// create a session provider to connect to the db
provider, err := db.NewSessionProvider(*opts)
if err != nil {
diff --git a/src/mongo/gotools/mongofiles/mongofiles.go b/src/mongo/gotools/mongofiles/mongofiles.go
index c2063185735..51f23c4f933 100644
--- a/src/mongo/gotools/mongofiles/mongofiles.go
+++ b/src/mongo/gotools/mongofiles/mongofiles.go
@@ -22,6 +22,7 @@ const (
List = "list"
Search = "search"
Put = "put"
+ PutID = "put_id"
Get = "get"
GetID = "get_id"
Delete = "delete"
@@ -48,6 +49,9 @@ type MongoFiles struct {
// filename in GridFS
FileName string
+
+ //ID to put into GridFS
+ Id string
}
// GFSFile represents a GridFS file.
@@ -67,25 +71,45 @@ func (mf *MongoFiles) ValidateCommand(args []string) error {
// too many arguments
if len(args) == 0 {
return fmt.Errorf("no command specified")
- } else if len(args) > 2 {
- return fmt.Errorf("too many positional arguments")
}
- var fileName string
switch args[0] {
case List:
+ if len(args) > 2 {
+ return fmt.Errorf("too many positional arguments")
+ }
if len(args) == 1 {
- fileName = ""
+ mf.FileName = ""
} else {
- fileName = args[1]
+ mf.FileName = args[1]
+ }
+ case Search, Put, Get, Delete:
+ if len(args) > 2 {
+ return fmt.Errorf("too many positional arguments")
}
- case Search, Put, Get, Delete, GetID, DeleteID:
// also make sure the supporting argument isn't literally an
// empty string for example, mongofiles get ""
if len(args) == 1 || args[1] == "" {
return fmt.Errorf("'%v' argument missing", args[0])
}
- fileName = args[1]
+ mf.FileName = args[1]
+ case GetID, DeleteID:
+ if len(args) > 2 {
+ return fmt.Errorf("too many positional arguments")
+ }
+ if len(args) == 1 || args[1] == "" {
+ return fmt.Errorf("'%v' argument missing", args[0])
+ }
+ mf.Id = args[1]
+ case PutID:
+ if len(args) > 3 {
+ return fmt.Errorf("too many positional arguments")
+ }
+ if len(args) < 3 || args[1] == "" || args[2] == "" {
+ return fmt.Errorf("'%v' argument(s) missing", args[0])
+ }
+ mf.FileName = args[1]
+ mf.Id = args[2]
default:
return fmt.Errorf("'%v' is not a valid command", args[0])
}
@@ -94,9 +118,7 @@ func (mf *MongoFiles) ValidateCommand(args []string) error {
return fmt.Errorf("--prefix can not be blank")
}
- // set the mongofiles command and file name
mf.Command = args[0]
- mf.FileName = fileName
return nil
}
@@ -155,9 +177,8 @@ func (mf *MongoFiles) handleGetID(gfs *mgo.GridFS) (string, error) {
// with the parsed _id, grab the file and write it to disk
gFile, err := gfs.OpenId(id)
if err != nil {
- return "", fmt.Errorf("error opening GridFS file with _id %s: %v", mf.FileName, err)
+ return "", fmt.Errorf("error opening GridFS file with _id %s: %v", mf.Id, err)
}
- log.Logvf(log.Always, "found file '%v' with _id %v", gFile.Name(), mf.FileName)
defer gFile.Close()
if err = mf.writeFile(gFile); err != nil {
return "", err
@@ -172,16 +193,16 @@ func (mf *MongoFiles) handleDeleteID(gfs *mgo.GridFS) (string, error) {
return "", err
}
if err = gfs.RemoveId(id); err != nil {
- return "", fmt.Errorf("error while removing file with _id %v from GridFS: %v\n", mf.FileName, err)
+ return "", fmt.Errorf("error while removing file with _id %v from GridFS: %v\n", mf.Id, err)
}
- return fmt.Sprintf("successfully deleted file with _id %v from GridFS\n", mf.FileName), nil
+ return fmt.Sprintf("successfully deleted file with _id %v from GridFS\n", mf.Id), nil
}
// parse and convert extended JSON
func (mf *MongoFiles) parseID() (interface{}, error) {
// parse the id using extended json
var asJSON interface{}
- err := json.Unmarshal([]byte(mf.FileName), &asJSON)
+ err := json.Unmarshal([]byte(mf.Id), &asJSON)
if err != nil {
return nil, fmt.Errorf(
"error parsing _id as json: %v; make sure you are properly escaping input", err)
@@ -213,8 +234,7 @@ func (mf *MongoFiles) writeFile(gridFile *mgo.GridFile) (err error) {
return nil
}
-// handle logic for 'put' command.
-func (mf *MongoFiles) handlePut(gfs *mgo.GridFS) (output string, err error) {
+func (mf *MongoFiles) handlePut(gfs *mgo.GridFS, hasID bool) (output string, err error) {
localFileName := mf.getLocalFileName(nil)
// check if --replace flag turned on
@@ -223,7 +243,8 @@ func (mf *MongoFiles) handlePut(gfs *mgo.GridFS) (output string, err error) {
if err != nil {
return "", err
}
- output = fmt.Sprintf("removed all instances of '%v' from GridFS\n", mf.FileName)
+ // always log that data has been removed
+ log.Logvf(log.Always, "removed all instances of '%v' from GridFS\n", mf.FileName)
}
var localFile io.ReadCloser
@@ -239,7 +260,7 @@ func (mf *MongoFiles) handlePut(gfs *mgo.GridFS) (output string, err error) {
log.Logvf(log.DebugLow, "creating GridFS file '%v' from local file '%v'", mf.FileName, localFileName)
}
- gFile, err := gfs.Create(mf.FileName)
+ gridFile, err := gfs.Create(mf.FileName)
if err != nil {
return "", fmt.Errorf("error while creating '%v' in GridFS: %v\n", mf.FileName, err)
}
@@ -247,24 +268,32 @@ func (mf *MongoFiles) handlePut(gfs *mgo.GridFS) (output string, err error) {
// GridFS files flush a buffer on Close(), so it's important we
// capture any errors that occur as this function exits and
// overwrite the error if earlier writes executed successfully
- if closeErr := gFile.Close(); err == nil && closeErr != nil {
+ if closeErr := gridFile.Close(); err == nil && closeErr != nil {
log.Logvf(log.DebugHigh, "error occurred while closing GridFS file handler")
err = fmt.Errorf("error while storing '%v' into GridFS: %v\n", localFileName, closeErr)
}
}()
+ if hasID {
+ id, err := mf.parseID()
+ if err != nil {
+ return "", err
+ }
+ gridFile.SetId(id)
+ }
+
// set optional mime type
if mf.StorageOptions.ContentType != "" {
- gFile.SetContentType(mf.StorageOptions.ContentType)
+ gridFile.SetContentType(mf.StorageOptions.ContentType)
}
- n, err := io.Copy(gFile, localFile)
+ n, err := io.Copy(gridFile, localFile)
if err != nil {
return "", fmt.Errorf("error while storing '%v' into GridFS: %v\n", localFileName, err)
}
log.Logvf(log.DebugLow, "copied %v bytes to server", n)
- output += fmt.Sprintf("added file: %v\n", gFile.Name())
+ output += fmt.Sprintf("added file: %v\n", gridFile.Name())
return output, nil
}
@@ -312,7 +341,9 @@ func (mf *MongoFiles) Run(displayHost bool) (string, error) {
log.Logvf(log.DebugLow, "connected to node type: %v", nodeType)
- safety, err := db.BuildWriteConcern(mf.StorageOptions.WriteConcern, nodeType)
+ safety, err := db.BuildWriteConcern(mf.StorageOptions.WriteConcern, nodeType,
+ mf.ToolOptions.URI.ParsedConnString())
+
if err != nil {
return "", fmt.Errorf("error parsing write concern: %v", err)
}
@@ -381,7 +412,14 @@ func (mf *MongoFiles) Run(displayHost bool) (string, error) {
case Put:
- output, err = mf.handlePut(gfs)
+ output, err = mf.handlePut(gfs, false)
+ if err != nil {
+ return "", err
+ }
+
+ case PutID:
+
+ output, err = mf.handlePut(gfs, true)
if err != nil {
return "", err
}
diff --git a/src/mongo/gotools/mongofiles/mongofiles_test.go b/src/mongo/gotools/mongofiles/mongofiles_test.go
index 2e0ad431ef8..675d8d3dc69 100644
--- a/src/mongo/gotools/mongofiles/mongofiles_test.go
+++ b/src/mongo/gotools/mongofiles/mongofiles_test.go
@@ -10,7 +10,6 @@ import (
"github.com/mongodb/mongo-tools/common/util"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/mgo.v2"
- "io"
"io/ioutil"
"os"
"strings"
@@ -33,6 +32,7 @@ var (
Connection: connection,
Auth: &auth,
Verbosity: &options.Verbosity{},
+ URI: &options.URI{},
}
)
@@ -89,8 +89,17 @@ func tearDownGridFSTestData() error {
return nil
}
+func simpleMongoFilesInstanceWithID(command, Id string) (*MongoFiles, error) {
+ return simpleMongoFilesInstanceWithFilenameAndID(command, "", Id)
+}
+func simpleMongoFilesInstanceWithFilename(command, fname string) (*MongoFiles, error) {
+ return simpleMongoFilesInstanceWithFilenameAndID(command, fname, "")
+}
+func simpleMongoFilesInstanceCommandOnly(command string) (*MongoFiles, error) {
+ return simpleMongoFilesInstanceWithFilenameAndID(command, "", "")
+}
-func simpleMongoFilesInstance(args []string) (*MongoFiles, error) {
+func simpleMongoFilesInstanceWithFilenameAndID(command, fname, Id string) (*MongoFiles, error) {
sessionProvider, err := db.NewSessionProvider(*toolOptions)
if err != nil {
return nil, err
@@ -101,13 +110,47 @@ func simpleMongoFilesInstance(args []string) (*MongoFiles, error) {
InputOptions: &InputOptions{},
StorageOptions: &StorageOptions{GridFSPrefix: "fs", DB: testDB},
SessionProvider: sessionProvider,
- Command: args[0],
- FileName: args[1],
+ Command: command,
+ FileName: fname,
+ Id: Id,
}
return &mongofiles, nil
}
+func fileContentsCompare(file1, file2 *os.File, t *testing.T) (bool, error) {
+ file1Stat, err := file1.Stat()
+ if err != nil {
+ return false, err
+ }
+
+ file2Stat, err := file2.Stat()
+ if err != nil {
+ return false, err
+ }
+
+ file1Size := file1Stat.Size()
+ file2Size := file2Stat.Size()
+
+ if file1Size != file2Size {
+ t.Log("file sizes not the same")
+ return false, nil
+ }
+
+ file1ContentsBytes, err := ioutil.ReadAll(file1)
+ if err != nil {
+ return false, err
+ }
+ file2ContentsBytes, err := ioutil.ReadAll(file2)
+ if err != nil {
+ return false, err
+ }
+
+ isContentSame := bytes.Compare(file1ContentsBytes, file2ContentsBytes) == 0
+ return isContentSame, nil
+
+}
+
// get an id of an existing file, for _id access
func idOfFile(mf *MongoFiles, filename string) string {
session, _ := mf.SessionProvider.GetSession()
@@ -146,6 +189,21 @@ func getFilesAndBytesFromLines(lines []string) ([]interface{}, []interface{}) {
return filesGotten, bytesGotten
}
+func getFilesAndBytesListFromGridFS() ([]interface{}, []interface{}, error) {
+ mfAfter, err := simpleMongoFilesInstanceCommandOnly("list")
+ if err != nil {
+ return nil, nil, err
+ }
+ str, err := mfAfter.Run(false)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ lines := cleanAndTokenizeTestOutput(str)
+ filesGotten, bytesGotten := getFilesAndBytesFromLines(lines)
+ return filesGotten, bytesGotten, nil
+}
+
// inefficient but fast way to ensure set equality of
func ensureSetEquality(firstArray []interface{}, secondArray []interface{}) {
for _, line := range firstArray {
@@ -169,8 +227,7 @@ func TestValidArguments(t *testing.T) {
testutil.VerifyTestType(t, testutil.UnitTestType)
Convey("With a MongoFiles instance", t, func() {
- args := []string{"search", "file"}
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithFilename("search", "file")
So(err, ShouldBeNil)
Convey("It should error out when no arguments fed", func() {
args := []string{}
@@ -179,21 +236,37 @@ func TestValidArguments(t *testing.T) {
So(err.Error(), ShouldEqual, "no command specified")
})
- Convey("It should error out when too many positional arguments provided", func() {
- args := []string{"list", "something", "another"}
+ Convey("(list|get|put|delete|search|get_id|delete_id) should error out when more than 1 positional argument provided", func() {
+ for _, command := range []string{"list", "get", "put", "delete", "search", "get_id", "delete_id"} {
+ args := []string{command, "arg1", "arg2"}
+ err := mf.ValidateCommand(args)
+ So(err, ShouldNotBeNil)
+ So(err.Error(), ShouldEqual, "too many positional arguments")
+ }
+ })
+
+ Convey("put_id should error out when more than 3 positional argument provided", func() {
+ args := []string{"put_id", "arg1", "arg2", "arg3"}
err := mf.ValidateCommand(args)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "too many positional arguments")
})
+ Convey("put_id should error out when only 1 positional argument provided", func() {
+ args := []string{"put_id", "arg1"}
+ err := mf.ValidateCommand(args)
+ So(err, ShouldNotBeNil)
+ So(err.Error(), ShouldEqual, fmt.Sprintf("'%v' argument(s) missing", "put_id"))
+ })
+
Convey("It should not error out when list command isn't given an argument", func() {
args := []string{"list"}
So(mf.ValidateCommand(args), ShouldBeNil)
So(mf.StorageOptions.LocalFileName, ShouldEqual, "")
})
- Convey("It should error out when any of (get|put|delete|search) not given supporting argument", func() {
- for _, command := range []string{"get", "put", "delete", "search"} {
+ Convey("It should error out when any of (get|put|delete|search|get_id|delete_id) not given supporting argument", func() {
+ for _, command := range []string{"get", "put", "delete", "search", "get_id", "delete_id"} {
args := []string{command}
err := mf.ValidateCommand(args)
So(err, ShouldNotBeNil)
@@ -226,9 +299,7 @@ func TestMongoFilesCommands(t *testing.T) {
filesExpected := []interface{}{"testfile1", "testfile2", "testfile3"}
Convey("Testing the 'list' command with a file that isn't in GridFS should", func() {
- args := []string{"list", "gibberish"}
-
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithFilename("list", "gibberish")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -240,9 +311,7 @@ func TestMongoFilesCommands(t *testing.T) {
})
Convey("Testing the 'list' command with files that are in GridFS should", func() {
- args := []string{"list", "testf"}
-
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithFilename("list", "testf")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -261,9 +330,7 @@ func TestMongoFilesCommands(t *testing.T) {
})
Convey("Testing the 'search' command with files that are in GridFS should", func() {
- args := []string{"search", "file"}
-
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithFilename("search", "file")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -282,9 +349,7 @@ func TestMongoFilesCommands(t *testing.T) {
})
Convey("Testing the 'get' command with a file that is in GridFS should", func() {
- args := []string{"get", "testfile1"}
-
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithFilename("get", "testfile1")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -338,12 +403,10 @@ func TestMongoFilesCommands(t *testing.T) {
Convey("Testing the 'get_id' command with a file that is in GridFS should", func() {
// hack to grab an _id
- args := []string{"get", "testfile1"}
- mf, _ := simpleMongoFilesInstance(args)
+ mf, _ := simpleMongoFilesInstanceWithFilename("get", "testfile1")
idString := idOfFile(mf, "testfile1")
- args = []string{"get_id", idString}
- mf, err = simpleMongoFilesInstance(args)
+ mf, err = simpleMongoFilesInstanceWithID("get_id", idString)
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -376,9 +439,7 @@ func TestMongoFilesCommands(t *testing.T) {
})
Convey("Testing the 'put' command by putting some lorem ipsum file with 287613 bytes should", func() {
- args := []string{"put", "lorem_ipsum_287613_bytes.txt"}
-
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithFilename("put", "lorem_ipsum_287613_bytes.txt")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
mf.StorageOptions.LocalFileName = util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt")
@@ -388,26 +449,15 @@ func TestMongoFilesCommands(t *testing.T) {
So(err, ShouldBeNil)
So(len(str), ShouldNotEqual, 0)
- Convey("and should have exactly 287613 bytes", func() {
- args = []string{"list", ""}
-
- mfAfter, err := simpleMongoFilesInstance(args)
+ Convey("and files should exist in gridfs", func() {
+ filesGotten, _, err := getFilesAndBytesListFromGridFS()
So(err, ShouldBeNil)
- So(mf, ShouldNotBeNil)
-
- str, err = mfAfter.Run(false)
- So(err, ShouldBeNil)
-
- lines := cleanAndTokenizeTestOutput(str)
- filesGotten, _ := getFilesAndBytesFromLines(lines)
- So(len(lines), ShouldEqual, len(filesExpected)+1)
+ So(len(filesGotten), ShouldEqual, len(filesExpected)+1)
So(filesGotten, ShouldContain, "lorem_ipsum_287613_bytes.txt")
})
Convey("and should have exactly the same content as the original file", func() {
- args = []string{"get", "lorem_ipsum_287613_bytes.txt"}
- So(err, ShouldBeNil)
- mfAfter, err := simpleMongoFilesInstance(args)
+ mfAfter, err := simpleMongoFilesInstanceWithFilename("get", "lorem_ipsum_287613_bytes.txt")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -422,32 +472,13 @@ func TestMongoFilesCommands(t *testing.T) {
loremIpsumCopy, err := os.Open("lorem_ipsum_copy.txt")
So(err, ShouldBeNil)
- Convey("compare the copy of the lorem ipsum file with the original 1KB at a time", func() {
- dataBytesOrig := make([]byte, 1024)
- dataBytesCopy := make([]byte, 1024)
+ Convey("compare the copy of the lorem ipsum file with the original", func() {
defer loremIpsumOrig.Close()
defer loremIpsumCopy.Close()
-
- var nReadOrig, nReadCopy int
-
- for {
- nReadOrig, err = loremIpsumOrig.Read(dataBytesOrig)
-
- // err should either be nil
- // or io.EOF --> indicating end of file
- So(err, ShouldBeIn, []error{nil, io.EOF})
-
- if nReadOrig == 0 {
- break
- }
-
- nReadCopy, err = loremIpsumCopy.Read(dataBytesCopy)
- So(err, ShouldBeNil)
-
- So(nReadOrig, ShouldEqual, nReadCopy)
- So(bytes.Compare(dataBytesOrig, dataBytesCopy), ShouldEqual, 0)
- }
+ isContentSame, err := fileContentsCompare(loremIpsumOrig, loremIpsumCopy, t)
+ So(err, ShouldBeNil)
+ So(isContentSame, ShouldBeTrue)
})
Reset(func() {
@@ -461,10 +492,14 @@ func TestMongoFilesCommands(t *testing.T) {
})
- Convey("Testing the 'delete' command with a file that is in GridFS should", func() {
- args := []string{"delete", "testfile2"}
+ Convey("Testing the 'put_id' command by putting some lorem ipsum file with 287613 bytes with different ids should succeed", func() {
+ for _, idToTest := range []string{"'test_id'", "'{a:\"b\"}'", "'{$numberlong:9999999999999999999999}'", "'{a:{b:{c:{}}}}'"} {
+ runPutIdTestCase(idToTest, t)
+ }
+ })
- mf, err := simpleMongoFilesInstance(args)
+ Convey("Testing the 'delete' command with a file that is in GridFS should", func() {
+ mf, err := simpleMongoFilesInstanceWithFilename("delete", "testfile2")
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -474,18 +509,9 @@ func TestMongoFilesCommands(t *testing.T) {
So(len(str), ShouldNotEqual, 0)
Convey("check that the file has been deleted from GridFS", func() {
- args = []string{"list", ""}
- mfAfter, err := simpleMongoFilesInstance(args)
- So(err, ShouldBeNil)
- So(mf, ShouldNotBeNil)
-
- str, err = mfAfter.Run(false)
- So(err, ShouldBeNil)
-
- lines := cleanAndTokenizeTestOutput(str)
- So(len(lines), ShouldEqual, len(filesExpected)-1)
-
- filesGotten, bytesGotten := getFilesAndBytesFromLines(lines)
+ filesGotten, bytesGotten, err := getFilesAndBytesListFromGridFS()
+ So(err, ShouldEqual, nil)
+ So(len(filesGotten), ShouldEqual, len(filesExpected)-1)
So(filesGotten, ShouldNotContain, "testfile2")
So(bytesGotten, ShouldNotContain, bytesExpected[1])
@@ -495,12 +521,10 @@ func TestMongoFilesCommands(t *testing.T) {
Convey("Testing the 'delete_id' command with a file that is in GridFS should", func() {
// hack to grab an _id
- args := []string{"get", "testfile2"}
- mf, _ := simpleMongoFilesInstance(args)
+ mf, _ := simpleMongoFilesInstanceWithFilename("get", "testfile2")
idString := idOfFile(mf, "testfile2")
- args = []string{"delete_id", idString}
- mf, err := simpleMongoFilesInstance(args)
+ mf, err := simpleMongoFilesInstanceWithID("delete_id", idString)
So(err, ShouldBeNil)
So(mf, ShouldNotBeNil)
@@ -510,18 +534,9 @@ func TestMongoFilesCommands(t *testing.T) {
So(len(str), ShouldNotEqual, 0)
Convey("check that the file has been deleted from GridFS", func() {
- args = []string{"list", ""}
- mfAfter, err := simpleMongoFilesInstance(args)
- So(err, ShouldBeNil)
- So(mf, ShouldNotBeNil)
-
- str, err = mfAfter.Run(false)
- So(err, ShouldBeNil)
-
- lines := cleanAndTokenizeTestOutput(str)
- So(len(lines), ShouldEqual, len(filesExpected)-1)
-
- filesGotten, bytesGotten := getFilesAndBytesFromLines(lines)
+ filesGotten, bytesGotten, err := getFilesAndBytesListFromGridFS()
+ So(err, ShouldEqual, nil)
+ So(len(filesGotten), ShouldEqual, len(filesExpected)-1)
So(filesGotten, ShouldNotContain, "testfile2")
So(bytesGotten, ShouldNotContain, bytesExpected[1])
@@ -535,3 +550,46 @@ func TestMongoFilesCommands(t *testing.T) {
})
}
+
+func runPutIdTestCase(idToTest string, t *testing.T) {
+ remoteName := "remoteName"
+ mongoFilesInstance, err := simpleMongoFilesInstanceWithFilenameAndID("put_id", remoteName, idToTest)
+
+ So(err, ShouldBeNil)
+ So(mongoFilesInstance, ShouldNotBeNil)
+ mongoFilesInstance.StorageOptions.LocalFileName = util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt")
+
+ t.Log("Should correctly insert the file into GridFS")
+ str, err := mongoFilesInstance.Run(false)
+ So(err, ShouldBeNil)
+ So(len(str), ShouldNotEqual, 0)
+
+ t.Log("and its filename should exist when the 'list' command is run")
+ filesGotten, _, err := getFilesAndBytesListFromGridFS()
+ So(err, ShouldBeNil)
+ So(filesGotten, ShouldContain, remoteName)
+
+ t.Log("and get_id should have exactly the same content as the original file")
+
+ mfAfter, err := simpleMongoFilesInstanceWithID("get_id", idToTest)
+ So(err, ShouldBeNil)
+ So(mfAfter, ShouldNotBeNil)
+
+ mfAfter.StorageOptions.LocalFileName = "lorem_ipsum_copy.txt"
+ str, err = mfAfter.Run(false)
+ So(err, ShouldBeNil)
+ So(len(str), ShouldNotEqual, 0)
+
+ loremIpsumOrig, err := os.Open(util.ToUniversalPath("testdata/lorem_ipsum_287613_bytes.txt"))
+ So(err, ShouldBeNil)
+
+ loremIpsumCopy, err := os.Open("lorem_ipsum_copy.txt")
+ So(err, ShouldBeNil)
+
+ defer loremIpsumOrig.Close()
+ defer loremIpsumCopy.Close()
+
+ isContentSame, err := fileContentsCompare(loremIpsumOrig, loremIpsumCopy, t)
+ So(err, ShouldBeNil)
+ So(isContentSame, ShouldBeTrue)
+}
diff --git a/src/mongo/gotools/mongofiles/options.go b/src/mongo/gotools/mongofiles/options.go
index 2df4774ebbc..ac16c7c48b9 100644
--- a/src/mongo/gotools/mongofiles/options.go
+++ b/src/mongo/gotools/mongofiles/options.go
@@ -8,6 +8,7 @@ Possible commands include:
list - list all files; 'filename' is an optional prefix which listed filenames must begin with
search - search all files; 'filename' is a substring which listed filenames must contain
put - add a file with filename 'filename'
+ put_id - add a file with filename 'filename' and a given '_id'
get - get a file with filename 'filename'
get_id - get a file with the given '_id'
delete - delete all files with filename 'filename'
@@ -34,7 +35,8 @@ type StorageOptions struct {
// Specifies the write concern for each write operation that mongofiles writes to the target database.
// By default, mongofiles waits for a majority of members from the replica set to respond before returning.
- WriteConcern string `long:"writeConcern" value-name:"<write-concern>" default:"majority" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}' (defaults to 'majority')"`
+ // Cannot be used simultaneously with write concern options in a URI.
+ WriteConcern string `long:"writeConcern" value-name:"<write-concern>" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}'"`
}
// Name returns a human-readable group name for storage options.
diff --git a/src/mongo/gotools/mongofiles/options_test.go b/src/mongo/gotools/mongofiles/options_test.go
new file mode 100644
index 00000000000..97fb48f51f6
--- /dev/null
+++ b/src/mongo/gotools/mongofiles/options_test.go
@@ -0,0 +1,74 @@
+package mongofiles
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/mongodb/mongo-tools/common/db"
+ "github.com/mongodb/mongo-tools/common/options"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+// Regression test for TOOLS-1741
+func TestWriteConcernWithURIParsing(t *testing.T) {
+ Convey("With an IngestOptions and ToolsOptions", t, func() {
+
+ // create an 'EnabledOptions' to determine what options should be able to be
+ // parsed and set form the input.
+ enabled := options.EnabledOptions{URI: true}
+
+ // create a new tools options to hold the parsed options
+ opts := options.New("", "", enabled)
+
+ // create a 'StorageOptions', which holds the value of the write concern
+ // for mongofiles.
+ storageOpts := &StorageOptions{}
+ opts.AddOptions(storageOpts)
+
+ // Specify that a write concern set on the URI is not an error and is a known
+ // possible option.
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsWriteConcern)
+
+ Convey("Parsing with no value should leave write concern empty", func() {
+ _, err := opts.ParseArgs([]string{})
+ So(err, ShouldBeNil)
+ So(storageOpts.WriteConcern, ShouldEqual, "")
+ Convey("and building write concern object, WMode should be majority", func() {
+ sessionSafety, err := db.BuildWriteConcern(storageOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldBeNil)
+ So(sessionSafety.WMode, ShouldEqual, "majority")
+ })
+ })
+
+ Convey("Parsing with no writeconcern in URI should not error", func() {
+ args := []string{
+ "--uri", "mongodb://localhost:27017/test",
+ }
+ _, err := opts.ParseArgs(args)
+ So(err, ShouldBeNil)
+ So(storageOpts.WriteConcern, ShouldEqual, "")
+ Convey("and parsing write concern, WMode should be majority", func() {
+ sessionSafety, err := db.BuildWriteConcern(storageOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldBeNil)
+ So(sessionSafety, ShouldNotBeNil)
+ So(sessionSafety.WMode, ShouldEqual, "majority")
+ })
+ })
+ Convey("Parsing with both writeconcern in URI and command line should error", func() {
+ args := []string{
+ "--uri", "mongodb://localhost:27017/test",
+ "--writeConcern", "majority",
+ }
+ _, err := opts.ParseArgs(args)
+ So(err, ShouldBeNil)
+ So(storageOpts.WriteConcern, ShouldEqual, "majority")
+ Convey("and parsing write concern, WMode should be majority", func() {
+ _, err := db.BuildWriteConcern(storageOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldResemble, fmt.Errorf("cannot specify writeConcern string and connectionString object"))
+ })
+ })
+ })
+}
diff --git a/src/mongo/gotools/mongoimport/main/mongoimport.go b/src/mongo/gotools/mongoimport/main/mongoimport.go
index 837e6d0ac26..6d63df2e99e 100644
--- a/src/mongo/gotools/mongoimport/main/mongoimport.go
+++ b/src/mongo/gotools/mongoimport/main/mongoimport.go
@@ -16,14 +16,15 @@ import (
func main() {
// initialize command-line opts
opts := options.New("mongoimport", mongoimport.Usage,
- options.EnabledOptions{Auth: true, Connection: true, Namespace: true})
+ options.EnabledOptions{Auth: true, Connection: true, Namespace: true, URI: true})
inputOpts := &mongoimport.InputOptions{}
opts.AddOptions(inputOpts)
ingestOpts := &mongoimport.IngestOptions{}
opts.AddOptions(ingestOpts)
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsWriteConcern)
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongoimport --help' for more information")
@@ -43,10 +44,8 @@ func main() {
return
}
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
// create a session provider to connect to the db
sessionProvider, err := db.NewSessionProvider(*opts)
diff --git a/src/mongo/gotools/mongoimport/mongoimport.go b/src/mongo/gotools/mongoimport/mongoimport.go
index 7fcdf91a990..2b2fde270b6 100644
--- a/src/mongo/gotools/mongoimport/mongoimport.go
+++ b/src/mongo/gotools/mongoimport/mongoimport.go
@@ -441,7 +441,8 @@ func (imp *MongoImport) ingestDocuments(readDocs chan bson.D) (retErr error) {
func (imp *MongoImport) configureSession(session *mgo.Session) error {
// sockets to the database will never be forcibly closed
session.SetSocketTimeout(0)
- sessionSafety, err := db.BuildWriteConcern(imp.IngestOptions.WriteConcern, imp.nodeType)
+ sessionSafety, err := db.BuildWriteConcern(imp.IngestOptions.WriteConcern, imp.nodeType,
+ imp.ToolOptions.ParsedConnString())
if err != nil {
return fmt.Errorf("write concern error: %v", err)
}
diff --git a/src/mongo/gotools/mongoimport/mongoimport_test.go b/src/mongo/gotools/mongoimport/mongoimport_test.go
index abc3d167975..248a4a45104 100644
--- a/src/mongo/gotools/mongoimport/mongoimport_test.go
+++ b/src/mongo/gotools/mongoimport/mongoimport_test.go
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"reflect"
+ "runtime"
"strings"
"testing"
@@ -63,12 +64,14 @@ func getBasicToolOptions() *options.ToolOptions {
Host: "localhost",
Port: db.DefaultTestPort,
}
+
return &options.ToolOptions{
General: general,
SSL: &ssl,
Namespace: namespace,
Connection: connection,
Auth: &auth,
+ URI: &options.URI{},
}
}
@@ -774,3 +777,23 @@ func TestImportDocuments(t *testing.T) {
})
})
}
+
+// Regression test for TOOLS-1694 to prevent issue from TOOLS-1115
+func TestHiddenOptionsDefaults(t *testing.T) {
+ Convey("With a new mongoimport with empty options", t, func() {
+ imp, err := NewMongoImport()
+ imp.ToolOptions = options.New("", "", options.EnabledOptions{})
+ So(err, ShouldBeNil)
+ Convey("Then parsing should fill args with expected defaults", func() {
+ _, err := imp.ToolOptions.ParseArgs([]string{})
+ So(err, ShouldBeNil)
+
+ // collection cannot be empty in validate
+ imp.ToolOptions.Collection = "col"
+ So(imp.ValidateSettings([]string{}), ShouldBeNil)
+ So(imp.IngestOptions.NumDecodingWorkers, ShouldEqual, runtime.NumCPU())
+ So(imp.IngestOptions.BulkBufferSize, ShouldEqual, 1000)
+ })
+ })
+
+}
diff --git a/src/mongo/gotools/mongoimport/options.go b/src/mongo/gotools/mongoimport/options.go
index 8ab9f9c9d47..1e59ad43f52 100644
--- a/src/mongo/gotools/mongoimport/options.go
+++ b/src/mongo/gotools/mongoimport/options.go
@@ -69,7 +69,9 @@ type IngestOptions struct {
UpsertFields string `long:"upsertFields" value-name:"<field>[,<field>]*" description:"comma-separated fields for the query part when --mode is set to upsert or merge"`
// Sets write concern level for write operations.
- WriteConcern string `long:"writeConcern" default:"majority" value-name:"<write-concern-specifier>" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}' (defaults to 'majority')"`
+ // By default mongoimport uses a write concern of 'majority'.
+ // Cannot be used simultaneously with write concern options in a URI.
+ WriteConcern string `long:"writeConcern" value-name:"<write-concern-specifier>" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}'"`
// Indicates that the server should bypass document validation on import.
BypassDocumentValidation bool `long:"bypassDocumentValidation" description:"bypass document validation"`
diff --git a/src/mongo/gotools/mongoimport/options_test.go b/src/mongo/gotools/mongoimport/options_test.go
new file mode 100644
index 00000000000..55950a5d5c7
--- /dev/null
+++ b/src/mongo/gotools/mongoimport/options_test.go
@@ -0,0 +1,74 @@
+package mongoimport
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/mongodb/mongo-tools/common/db"
+ "github.com/mongodb/mongo-tools/common/options"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+// Regression test for TOOLS-1741
+func TestWriteConcernWithURIParsing(t *testing.T) {
+ Convey("With an IngestOptions and ToolsOptions", t, func() {
+
+ // create an 'EnabledOptions' to determine what options should be able to be
+ // parsed and set form the input.
+ enabled := options.EnabledOptions{URI: true}
+
+ // create a new tools options to hold the parsed options
+ opts := options.New("", "", enabled)
+
+ // create an 'IngestOptions', which holds the value of the write concern
+ // for mongoimport.
+ ingestOpts := &IngestOptions{}
+ opts.AddOptions(ingestOpts)
+
+ // Specify that a write concern set on the URI is not an error and is a known
+ // possible option.
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsWriteConcern)
+
+ Convey("Parsing with no value should leave write concern empty", func() {
+ _, err := opts.ParseArgs([]string{})
+ So(err, ShouldBeNil)
+ So(ingestOpts.WriteConcern, ShouldEqual, "")
+ Convey("and building write concern object, WMode should be majority", func() {
+ sessionSafety, err := db.BuildWriteConcern(ingestOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldBeNil)
+ So(sessionSafety.WMode, ShouldEqual, "majority")
+ })
+ })
+
+ Convey("Parsing with no writeconcern in URI should not error", func() {
+ args := []string{
+ "--uri", "mongodb://localhost:27017/test",
+ }
+ _, err := opts.ParseArgs(args)
+ So(err, ShouldBeNil)
+ So(ingestOpts.WriteConcern, ShouldEqual, "")
+ Convey("and parsing write concern, WMode should be majority", func() {
+ sessionSafety, err := db.BuildWriteConcern(ingestOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldBeNil)
+ So(sessionSafety, ShouldNotBeNil)
+ So(sessionSafety.WMode, ShouldEqual, "majority")
+ })
+ })
+ Convey("Parsing with both writeconcern in URI and command line should error", func() {
+ args := []string{
+ "--uri", "mongodb://localhost:27017/test",
+ "--writeConcern", "majority",
+ }
+ _, err := opts.ParseArgs(args)
+ So(err, ShouldBeNil)
+ So(ingestOpts.WriteConcern, ShouldEqual, "majority")
+ Convey("and parsing write concern, WMode should be majority", func() {
+ _, err := db.BuildWriteConcern(ingestOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldResemble, fmt.Errorf("cannot specify writeConcern string and connectionString object"))
+ })
+ })
+ })
+}
diff --git a/src/mongo/gotools/mongooplog/main/mongooplog.go b/src/mongo/gotools/mongooplog/main/mongooplog.go
index a9215a64c36..c038db057da 100644
--- a/src/mongo/gotools/mongooplog/main/mongooplog.go
+++ b/src/mongo/gotools/mongooplog/main/mongooplog.go
@@ -14,7 +14,7 @@ import (
func main() {
// initialize command line options
opts := options.New("mongooplog", mongooplog.Usage,
- options.EnabledOptions{Auth: true, Connection: true, Namespace: false})
+ options.EnabledOptions{Auth: true, Connection: true, Namespace: false, URI: true})
// add the mongooplog-specific options
sourceOpts := &mongooplog.SourceOptions{}
@@ -23,7 +23,7 @@ func main() {
log.Logvf(log.Always, "warning: mongooplog is deprecated, and will be removed completely in a future release")
// parse the command line options
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongooplog --help' for more information")
@@ -50,10 +50,8 @@ func main() {
log.SetVerbosity(opts.Verbosity)
signals.Handle()
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
// validate the mongooplog options
if sourceOpts.From == "" {
diff --git a/src/mongo/gotools/mongoreplay/auth_test.go b/src/mongo/gotools/mongoreplay/auth_test.go
index 58ba964a753..509d998601c 100644
--- a/src/mongo/gotools/mongoreplay/auth_test.go
+++ b/src/mongo/gotools/mongoreplay/auth_test.go
@@ -30,15 +30,23 @@ func TestCommandsAgainstAuthedDBWhenAuthed(t *testing.T) {
}
}()
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(urlAuth)
+ if err != nil {
+ t.Error(err)
+ }
+
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", urlAuth)
- err := Play(context, generator.opChan, testSpeed, urlAuth, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Error(err)
}
t.Log("Completed mongoreplay playback of generated traffic")
session, err := mgo.Dial(urlAuth)
+ if err != nil {
+ t.Error(err)
+ }
coll := session.DB(testDB).C(testCollection)
iter := coll.Find(bson.D{}).Sort("docNum").Iter()
@@ -95,14 +103,21 @@ func TestCommandsAgainstAuthedDBWhenNotAuthed(t *testing.T) {
}
}()
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
- context := NewExecutionContext(statCollector)
- err := Play(context, generator.opChan, testSpeed, urlNonAuth, 1, 10)
+ replaySession, err := mgo.Dial(urlNonAuth)
+ if err != nil {
+ t.Error(err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Error(err)
}
t.Log("Completed mongoreplay playback of generated traffic")
session, err := mgo.Dial(urlAuth)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
coll := session.DB(testDB).C(testCollection)
t.Log("Performing query to ensure collection received no documents")
diff --git a/src/mongo/gotools/mongoreplay/command_op.go b/src/mongo/gotools/mongoreplay/command_op.go
index 17499194a5e..601aca845b8 100644
--- a/src/mongo/gotools/mongoreplay/command_op.go
+++ b/src/mongo/gotools/mongoreplay/command_op.go
@@ -237,11 +237,9 @@ func (op *CommandOp) FromReader(r io.Reader) error {
// Execute performs the CommandOp on a given session, yielding the reply when
// successful (and an error otherwise).
-func (op *CommandOp) Execute(session *mgo.Session) (Replyable, error) {
- session.SetSocketTimeout(0)
-
+func (op *CommandOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
before := time.Now()
- metadata, commandReply, replyData, resultReply, err := mgo.ExecOpWithReply(session, &op.CommandOp)
+ metadata, commandReply, replyData, resultReply, err := mgo.ExecOpWithReply(socket, &op.CommandOp)
after := time.Now()
if err != nil {
return nil, err
diff --git a/src/mongo/gotools/mongoreplay/command_reply.go b/src/mongo/gotools/mongoreplay/command_reply.go
index 55d0c5dd6ea..df42afcc1f1 100644
--- a/src/mongo/gotools/mongoreplay/command_reply.go
+++ b/src/mongo/gotools/mongoreplay/command_reply.go
@@ -174,7 +174,7 @@ func (op *CommandReplyOp) FromReader(r io.Reader) error {
// Execute logs a warning and returns nil because OP_COMMANDREPLY cannot yet be
// handled fully by mongoreplay.
-func (op *CommandReplyOp) Execute(session *mgo.Session) (Replyable, error) {
+func (op *CommandReplyOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
userInfoLogger.Logv(Always, "Skipping unimplemented op: OP_COMMANDREPLY")
return nil, nil
}
diff --git a/src/mongo/gotools/mongoreplay/delete_op.go b/src/mongo/gotools/mongoreplay/delete_op.go
index 329a6f3f630..e896374dc7b 100644
--- a/src/mongo/gotools/mongoreplay/delete_op.go
+++ b/src/mongo/gotools/mongoreplay/delete_op.go
@@ -92,9 +92,8 @@ func (op *DeleteOp) FromReader(r io.Reader) error {
// Execute performs the DeleteOp on a given session, yielding the reply when
// successful (and an error otherwise).
-func (op *DeleteOp) Execute(session *mgo.Session) (Replyable, error) {
- session.SetSocketTimeout(0)
- if err := mgo.ExecOpWithoutReply(session, &op.DeleteOp); err != nil {
+func (op *DeleteOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
+ if err := mgo.ExecOpWithoutReply(socket, &op.DeleteOp); err != nil {
return nil, err
}
return nil, nil
diff --git a/src/mongo/gotools/mongoreplay/execute.go b/src/mongo/gotools/mongoreplay/execute.go
index ff3e58104e4..ea314d3c0b4 100644
--- a/src/mongo/gotools/mongoreplay/execute.go
+++ b/src/mongo/gotools/mongoreplay/execute.go
@@ -40,18 +40,37 @@ type ExecutionContext struct {
// ExecutionContext
sync.Mutex
- SessionChansWaitGroup sync.WaitGroup
+ ConnectionChansWaitGroup sync.WaitGroup
*StatCollector
+
+ // fullSpeed is a control to indicate whether the tool will sleep to synchronize
+ // the playback of operations or if it will play back all operations as fast
+ // as possible.
+ fullSpeed bool
+
+ driverOpsFiltered bool
+
+ session *mgo.Session
+}
+
+// ExecutionOptions holds the additional configuration options needed to completely
+// create an execution session.
+type ExecutionOptions struct {
+ fullSpeed bool
+ driverOpsFiltered bool
}
// NewExecutionContext initializes a new ExecutionContext.
-func NewExecutionContext(statColl *StatCollector) *ExecutionContext {
+func NewExecutionContext(statColl *StatCollector, session *mgo.Session, options *ExecutionOptions) *ExecutionContext {
return &ExecutionContext{
IncompleteReplies: cache.New(60*time.Second, 60*time.Second),
CompleteReplies: map[string]*ReplyPair{},
CursorIDMap: newCursorCache(),
StatCollector: statColl,
+ fullSpeed: options.fullSpeed,
+ driverOpsFiltered: options.driverOpsFiltered,
+ session: session,
}
}
@@ -120,7 +139,7 @@ func (context *ExecutionContext) rewriteCursors(rewriteable cursorsRewriteable,
func (context *ExecutionContext) handleCompletedReplies() error {
context.Lock()
for key, rp := range context.CompleteReplies {
- userInfoLogger.Logvf(DebugHigh, "Completed reply: %#v, %#v", rp.ops[ReplyFromFile], rp.ops[ReplyFromWire])
+ userInfoLogger.Logvf(DebugHigh, "Completed reply: %v, %v", rp.ops[ReplyFromFile], rp.ops[ReplyFromWire])
cursorFromFile, err := rp.ops[ReplyFromFile].getCursorID()
if err != nil {
return err
@@ -140,20 +159,19 @@ func (context *ExecutionContext) handleCompletedReplies() error {
return nil
}
-func (context *ExecutionContext) newExecutionSession(url string, start time.Time, connectionNum int64) chan<- *RecordedOp {
-
+func (context *ExecutionContext) newExecutionConnection(start time.Time, connectionNum int64) chan<- *RecordedOp {
ch := make(chan *RecordedOp, 10000)
+ context.ConnectionChansWaitGroup.Add(1)
- context.SessionChansWaitGroup.Add(1)
go func() {
now := time.Now()
var connected bool
time.Sleep(start.Add(-5 * time.Second).Sub(now)) // Sleep until five seconds before the start time
- session, err := mgo.Dial(url)
+ socket, err := context.session.AcquireSocketDirect()
if err == nil {
userInfoLogger.Logvf(Info, "(Connection %v) New connection CREATED.", connectionNum)
connected = true
- defer session.Close()
+ defer socket.Close()
} else {
userInfoLogger.Logvf(Info, "(Connection %v) New Connection FAILED: %v", connectionNum, err)
}
@@ -167,38 +185,38 @@ func (context *ExecutionContext) newExecutionSession(url string, start time.Time
// This allows it to be used for downstream reporting of stats.
recordedOp.PlayedConnectionNum = connectionNum
t := time.Now()
- if recordedOp.RawOp.Header.OpCode != OpCodeReply {
+
+ if !context.fullSpeed && recordedOp.RawOp.Header.OpCode != OpCodeReply {
if t.Before(recordedOp.PlayAt.Time) {
time.Sleep(recordedOp.PlayAt.Sub(t))
}
}
userInfoLogger.Logvf(DebugHigh, "(Connection %v) op %v", connectionNum, recordedOp.String())
- session.SetSocketTimeout(0)
- parsedOp, reply, err = context.Execute(recordedOp, session)
+ parsedOp, reply, err = context.Execute(recordedOp, socket)
if err != nil {
toolDebugLogger.Logvf(Always, "context.Execute error: %v", err)
}
} else {
parsedOp, err = recordedOp.Parse()
if err != nil {
- toolDebugLogger.Logvf(Always, "Execution Session error: %v", err)
+ toolDebugLogger.Logvf(Always, "Execution Connection error: %v", err)
}
- msg = fmt.Sprintf("Skipped on non-connected session (Connection %v)", connectionNum)
+ msg = fmt.Sprintf("Skipped on non-connected socket (Connection %v)", connectionNum)
toolDebugLogger.Logv(Always, msg)
}
- if shouldCollectOp(parsedOp) {
+ if shouldCollectOp(parsedOp, context.driverOpsFiltered) {
context.Collect(recordedOp, parsedOp, reply, msg)
}
}
userInfoLogger.Logvf(Info, "(Connection %v) Connection ENDED.", connectionNum)
- context.SessionChansWaitGroup.Done()
+ context.ConnectionChansWaitGroup.Done()
}()
return ch
}
-// Execute plays a particular command on an mgo session.
-func (context *ExecutionContext) Execute(op *RecordedOp, session *mgo.Session) (Op, Replyable, error) {
+// Execute plays a particular command on an mgo socket.
+func (context *ExecutionContext) Execute(op *RecordedOp, socket *mgo.MongoSocket) (Op, Replyable, error) {
opToExec, err := op.RawOp.Parse()
var reply Replyable
@@ -214,7 +232,7 @@ func (context *ExecutionContext) Execute(op *RecordedOp, session *mgo.Session) (
} else if recordedCommandReply, ok := opToExec.(*CommandReplyOp); ok {
context.AddFromFile(recordedCommandReply, op)
} else {
- if IsDriverOp(opToExec) {
+ if !context.driverOpsFiltered && IsDriverOp(opToExec) {
return opToExec, nil, nil
}
@@ -230,7 +248,7 @@ func (context *ExecutionContext) Execute(op *RecordedOp, session *mgo.Session) (
op.PlayedAt = &PreciseTime{time.Now()}
- reply, err = opToExec.Execute(session)
+ reply, err = opToExec.Execute(socket)
if err != nil {
context.CursorIDMap.MarkFailed(op)
@@ -243,6 +261,5 @@ func (context *ExecutionContext) Execute(op *RecordedOp, session *mgo.Session) (
}
context.handleCompletedReplies()
-
return opToExec, reply, nil
}
diff --git a/src/mongo/gotools/mongoreplay/execute_test.go b/src/mongo/gotools/mongoreplay/execute_test.go
index 1b4d23559d4..81d58d772a0 100644
--- a/src/mongo/gotools/mongoreplay/execute_test.go
+++ b/src/mongo/gotools/mongoreplay/execute_test.go
@@ -7,7 +7,7 @@ import (
)
func TestCompleteReply(t *testing.T) {
- context := NewExecutionContext(&StatCollector{})
+ context := NewExecutionContext(&StatCollector{}, nil, &ExecutionOptions{})
// AddFromWire takes a recorded request and a live reply to the re-execution
// of that reply
diff --git a/src/mongo/gotools/mongoreplay/filter.go b/src/mongo/gotools/mongoreplay/filter.go
new file mode 100644
index 00000000000..a6593d5fdd7
--- /dev/null
+++ b/src/mongo/gotools/mongoreplay/filter.go
@@ -0,0 +1,173 @@
+package mongoreplay
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "sync"
+ "time"
+)
+
+// FilterCommand stores settings for the mongoreplay 'filter' subcommand
+type FilterCommand struct {
+ GlobalOpts *Options `no-flag:"true"`
+ PlaybackFile string `description:"path to the playback file to read from" short:"p" long:"playback-file" required:"yes"`
+ OutFile string `description:"path to the output file to write to" short:"o" long:"outputFile"`
+ SplitFilePrefix string `description:"prefix file name to use for the output files being written when splitting traffic" long:"outfilePrefix"`
+ StartTime string `description:"ISO 8601 timestamp to remove all operations before" long:"startAt"`
+ Split int `description:"split the traffic into n files with roughly equal numbers of connecitons in each" default:"1" long:"split"`
+ RemoveDriverOps bool `description:"remove driver issued operations from the playback" long:"removeDriverOps"`
+ Gzip bool `long:"gzip" description:"decompress gzipped input"`
+
+ startTime time.Time
+}
+
+// Execute runs the program for the 'filter' subcommand
+func (filter *FilterCommand) Execute(args []string) error {
+ err := filter.ValidateParams(args)
+ if err != nil {
+ return err
+ }
+ filter.GlobalOpts.SetLogging()
+
+ playbackFileReader, err := NewPlaybackFileReader(filter.PlaybackFile, filter.Gzip)
+ if err != nil {
+ return err
+ }
+ opChan, errChan := playbackFileReader.OpChan(1)
+
+ driverOpsFiltered := filter.RemoveDriverOps || playbackFileReader.metadata.DriverOpsFiltered
+
+ outfiles := make([]*PlaybackFileWriter, filter.Split)
+ if filter.Split == 1 {
+ playbackWriter, err := NewPlaybackFileWriter(filter.OutFile, driverOpsFiltered,
+ filter.Gzip)
+ if err != nil {
+ return err
+ }
+ outfiles[0] = playbackWriter
+ } else {
+ for i := 0; i < filter.Split; i++ {
+ playbackWriter, err := NewPlaybackFileWriter(
+ fmt.Sprintf("%s%02d.playback", filter.SplitFilePrefix, i), driverOpsFiltered,
+ filter.Gzip)
+ if err != nil {
+ return err
+ }
+ outfiles[i] = playbackWriter
+ defer playbackWriter.Close()
+ }
+ }
+
+ if err := Filter(opChan, outfiles, filter.RemoveDriverOps, filter.startTime); err != nil {
+ userInfoLogger.Logvf(Always, "Filter: %v\n", err)
+ }
+
+ //handle the error from the errchan
+ err = <-errChan
+ if err != nil && err != io.EOF {
+ userInfoLogger.Logvf(Always, "OpChan: %v", err)
+ }
+ return nil
+}
+
+func Filter(opChan <-chan *RecordedOp,
+ outfiles []*PlaybackFileWriter,
+ removeDriverOps bool,
+ truncateTime time.Time) error {
+
+ opWriters := make([]chan<- *RecordedOp, len(outfiles))
+ errChan := make(chan error)
+ wg := &sync.WaitGroup{}
+
+ for i := range outfiles {
+ opWriters[i] = newParallelPlaybackWriter(outfiles[i], errChan, wg)
+ }
+ for op := range opChan {
+ // if specified, bypass driver operations
+ if removeDriverOps {
+ parsedOp, err := op.RawOp.Parse()
+ if err != nil {
+ return err
+ }
+ if IsDriverOp(parsedOp) {
+ continue
+ }
+ }
+ // if specified, ignore ops before the given timestamp
+ // if truncateTime not specified, it will be time zero and all
+ // operation times will be greater than it
+ if op.Seen.Time.Before(truncateTime) {
+ continue
+ }
+ fileNum := op.SeenConnectionNum % int64(len(outfiles))
+ opWriters[fileNum] <- op
+ }
+ for _, opWriter := range opWriters {
+ close(opWriter)
+ }
+ wg.Wait()
+ close(errChan)
+
+ var hasError bool
+ for err := range errChan {
+ hasError = true
+ userInfoLogger.Logvf(Always, "error: %s", err)
+ }
+ if hasError {
+ return fmt.Errorf("errors encountered while running filter")
+ }
+
+ return nil
+}
+
+func newParallelPlaybackWriter(outfile *PlaybackFileWriter,
+ errChan chan<- error, wg *sync.WaitGroup) chan<- *RecordedOp {
+ var didWriteOp bool
+
+ inputOpChan := make(chan *RecordedOp, 1000)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for op := range inputOpChan {
+ err := bsonToWriter(outfile, op)
+ if err != nil {
+ errChan <- err
+ return
+ }
+ didWriteOp = true
+ }
+ if !didWriteOp {
+ userInfoLogger.Logvf(Always, "no connections written to file %s, removing", outfile.fname)
+ err := os.Remove(outfile.fname)
+ if err != nil {
+ errChan <- err
+ return
+ }
+ }
+ }()
+ return inputOpChan
+}
+
+func (filter *FilterCommand) ValidateParams(args []string) error {
+ switch {
+ case filter.Split < 1:
+ return fmt.Errorf("must be a positive number of files to split into")
+ case filter.Split > 1 && filter.SplitFilePrefix == "":
+ return fmt.Errorf("must specify a filename prefix when splitting traffic")
+ case filter.Split > 1 && filter.OutFile != "":
+ return fmt.Errorf("must not specify an output file name when splitting traffic" +
+ "instead only specify a file name prefix")
+ case filter.Split == 1 && filter.OutFile == "":
+ return fmt.Errorf("must specify an output file")
+ }
+
+ if filter.StartTime != "" {
+ t, err := time.Parse(time.RFC3339, filter.StartTime)
+ if err != nil {
+ return fmt.Errorf("error parsing start time argument: %v", err)
+ }
+ filter.startTime = t
+ }
+ return nil
+}
diff --git a/src/mongo/gotools/mongoreplay/filter_test.go b/src/mongo/gotools/mongoreplay/filter_test.go
new file mode 100644
index 00000000000..74a6698d2e5
--- /dev/null
+++ b/src/mongo/gotools/mongoreplay/filter_test.go
@@ -0,0 +1,316 @@
+package mongoreplay
+
+import (
+ "bytes"
+ "io"
+ "testing"
+ "time"
+
+ "github.com/10gen/llmgo/bson"
+)
+
+func TestRemoveDriverOpsFromFile(t *testing.T) {
+ cases := []struct {
+ name string
+
+ driverOpsToGenerate []string
+ numInsertsToGenerate int
+ shouldRemoveDriverOps bool
+
+ numOpsExpectedAfterFilter int
+ }{
+ {
+ "filter driver ops",
+
+ []string{"isMaster", "ping", "getnonce"},
+ 4,
+ true,
+
+ 4,
+ },
+ {
+ "no driver ops in file",
+
+ []string{},
+ 4,
+ true,
+
+ 4,
+ },
+ {
+ "don't filter driver ops",
+
+ []string{"isMaster", "ping", "getnonce"},
+ 4,
+ false,
+
+ 7,
+ },
+ }
+ for _, c := range cases {
+ t.Logf("running case: %s\n", c.name)
+ // make an iowriter that just buffers
+ b := &bytes.Buffer{}
+ bufferFile := NopWriteCloser(b)
+
+ playbackWriter, err := playbackFileWriterFromWriteCloser(bufferFile, "file", PlaybackFileMetadata{})
+ if err != nil {
+ t.Fatalf("couldn't create playbackfile writer %v", err)
+ }
+
+ // start a goroutine to write recorded ops to the opChan
+ generator := newRecordedOpGenerator()
+ go func() {
+ defer close(generator.opChan)
+ t.Logf("Generating %d inserts\n", c.numInsertsToGenerate)
+ err := generator.generateInsertHelper("insert", 0, c.numInsertsToGenerate)
+ if err != nil {
+ t.Error(err)
+ }
+ t.Log("Generating driver ops")
+ for _, opName := range c.driverOpsToGenerate {
+ err = generator.generateCommandOp(opName, bson.D{}, 123)
+ if err != nil {
+ t.Error(err)
+ }
+ }
+ }()
+
+ // run Filter to remove the driver op from the file
+ if err := Filter(generator.opChan, []*PlaybackFileWriter{playbackWriter}, c.shouldRemoveDriverOps, time.Time{}); err != nil {
+ t.Error(err)
+ }
+
+ rs := bytes.NewReader(b.Bytes())
+ // open a reader into the written output
+ playbackReader, err := playbackFileReaderFromReadSeeker(rs, "")
+ if err != nil {
+ t.Fatalf("couldn't create playbackfile reader %v", err)
+ }
+ opChan, errChan := playbackReader.OpChan(1)
+
+ // loop over the found operations and verify that the correct number and
+ // types of operations are found
+ numOpsFound := 0
+ numDriverOpsFound := 0
+ for op := range opChan {
+ numOpsFound++
+ parsedOp, err := op.RawOp.Parse()
+ if err != nil {
+ t.Error(err)
+ }
+
+ if IsDriverOp(parsedOp) {
+ numDriverOpsFound++
+ }
+ }
+
+ if c.shouldRemoveDriverOps && numDriverOpsFound > 0 {
+ t.Errorf("expected to have removed driver ops but instead found %d", numDriverOpsFound)
+ }
+
+ if c.numOpsExpectedAfterFilter != numOpsFound {
+ t.Errorf("expected to have found %d total ops after filter but instead found %d", c.numOpsExpectedAfterFilter, numOpsFound)
+ }
+ err = <-errChan
+ if err != io.EOF {
+ t.Errorf("should have eof at end, but got %v", err)
+ }
+ }
+}
+
+func TestSplitInputFile(t *testing.T) {
+ cases := []struct {
+ name string
+
+ numPlaybackFiles int
+ numConnections int
+ numOpsPerConnection int
+ }{
+ {
+ "one file",
+ 1,
+ 10,
+ 10,
+ },
+ {
+ "multi file",
+ 5,
+ 10,
+ 2,
+ },
+ }
+ for _, c := range cases {
+ t.Logf("running case: %s\n", c.name)
+ outfiles := make([]*PlaybackFileWriter, c.numPlaybackFiles)
+ buffers := make([]*bytes.Buffer, c.numPlaybackFiles)
+
+ // create a buffer to represent each specified playback file to write
+ for i := 0; i < c.numPlaybackFiles; i++ {
+ b := &bytes.Buffer{}
+ buffers[i] = b
+
+ bufferFile := NopWriteCloser(b)
+ playbackWriter, err := playbackFileWriterFromWriteCloser(bufferFile, "testfile", PlaybackFileMetadata{})
+ if err != nil {
+ t.Fatalf("couldn't create playbackfile writer %v", err)
+ }
+ outfiles[i] = playbackWriter
+ }
+
+ // make an channel to push all recorded connections into
+ opChan := make(chan *RecordedOp)
+ go func() {
+ t.Logf("generating %d recorded connections\n", c.numConnections)
+ for i := 0; i < c.numConnections; i++ {
+ generator := newRecordedOpGenerator()
+ generator.generateInsertHelper("insert", 0, c.numOpsPerConnection)
+ close(generator.opChan)
+ for recordedOp := range generator.opChan {
+ recordedOp.SeenConnectionNum = int64(i)
+ opChan <- recordedOp
+ }
+ }
+ close(opChan)
+ }()
+
+ // run the main filter routine with the given input
+ if err := Filter(opChan, outfiles, false, time.Time{}); err != nil {
+ t.Error(err)
+ }
+
+ // ensure that each file contains only ops from the connection determined by
+ // connectionNum % numFiles == filenum
+ t.Log("verifying connections correctly split")
+ for fileNum, writtenBuffer := range buffers {
+ rs := bytes.NewReader(writtenBuffer.Bytes())
+ playbackReader, err := playbackFileReaderFromReadSeeker(rs, "")
+ if err != nil {
+ t.Fatalf("couldn't create playbackfile reader %v", err)
+ }
+ opChan, errChan := playbackReader.OpChan(1)
+
+ for op := range opChan {
+ expectedFileNum := op.SeenConnectionNum % int64(len(outfiles))
+ if expectedFileNum != int64(fileNum) {
+ t.Errorf("expected op with connection number %d to be in file"+
+ "%d, but instead it was found in file %d", op.SeenConnectionNum, expectedFileNum, fileNum)
+ }
+ }
+ err = <-errChan
+ if err != io.EOF {
+ t.Errorf("should have eof at end, but got %v", err)
+ }
+ }
+ }
+}
+
+func TestRemoveOpsBeforeTime(t *testing.T) {
+ // array of times to use for testing
+ timesForTest := make([]time.Time, 16)
+ now := time.Now()
+ for i := range timesForTest {
+ timesForTest[i] = now.Add(time.Second * time.Duration(i))
+ }
+
+ cases := []struct {
+ name string
+
+ timeToTruncateBefore time.Time
+ timesOfRecordedOps []time.Time
+
+ numOpsExpectedAfterFilter int
+ }{
+ {
+ "no truncation",
+
+ time.Time{},
+ timesForTest,
+ 16,
+ },
+ {
+ "truncate all but one",
+
+ timesForTest[len(timesForTest)-1],
+ timesForTest,
+ 1,
+ },
+ {
+ "truncate half",
+
+ timesForTest[(len(timesForTest))/2],
+ timesForTest,
+
+ 8,
+ },
+ }
+ for _, c := range cases {
+ t.Logf("running case: %s\n", c.name)
+
+ // create a bytes buffer to write output into
+ b := &bytes.Buffer{}
+ bufferFile := NopWriteCloser(b)
+
+ playbackWriter, err := playbackFileWriterFromWriteCloser(bufferFile, "file", PlaybackFileMetadata{})
+ if err != nil {
+ t.Fatalf("couldn't create playbackfile writer %v", err)
+ }
+
+ //create a recorded op for each time specified
+ inputOpChan := make(chan *RecordedOp)
+ go func() {
+ generator := newRecordedOpGenerator()
+ generator.generateInsertHelper("insert", 0, len(c.timesOfRecordedOps))
+ close(generator.opChan)
+ i := 0
+ for recordedOp := range generator.opChan {
+ recordedOp.Seen = &PreciseTime{c.timesOfRecordedOps[i]}
+ inputOpChan <- recordedOp
+ i++
+ }
+ close(inputOpChan)
+ }()
+
+ // run the main filter routine with the given input
+ if err := Filter(inputOpChan, []*PlaybackFileWriter{playbackWriter}, false, c.timeToTruncateBefore); err != nil {
+ t.Error(err)
+ }
+
+ rs := bytes.NewReader(b.Bytes())
+ playbackReader, err := playbackFileReaderFromReadSeeker(rs, "")
+ if err != nil {
+ t.Fatalf("couldn't create playbackfile reader %v", err)
+ }
+ resultOpChan, errChan := playbackReader.OpChan(1)
+
+ numOpsSeen := 0
+ for op := range resultOpChan {
+ numOpsSeen++
+ if op.Seen.Time.Before(c.timeToTruncateBefore) {
+ t.Errorf("execpected op with time %v to be truncated", op.Seen.Time)
+ }
+ }
+
+ if numOpsSeen != c.numOpsExpectedAfterFilter {
+ t.Errorf("expected to see %d ops but instead saw %d", c.numOpsExpectedAfterFilter, numOpsSeen)
+ }
+
+ err = <-errChan
+ if err != io.EOF {
+ t.Errorf("should have eof at end, but got %v", err)
+ }
+ }
+}
+
+// convienence function for adding a close method to an io.Writer
+func NopWriteCloser(w io.Writer) io.WriteCloser {
+ return &nopWriteCloser{w}
+}
+
+type nopWriteCloser struct {
+ io.Writer
+}
+
+func (wc *nopWriteCloser) Close() error {
+ return nil
+}
diff --git a/src/mongo/gotools/mongoreplay/getmore_op.go b/src/mongo/gotools/mongoreplay/getmore_op.go
index 1fbd322864f..6619cae048d 100644
--- a/src/mongo/gotools/mongoreplay/getmore_op.go
+++ b/src/mongo/gotools/mongoreplay/getmore_op.go
@@ -83,11 +83,10 @@ func (op *GetMoreOp) FromReader(r io.Reader) error {
// Execute performs the GetMoreOp on a given session, yielding the reply when
// successful (and an error otherwise).
-func (op *GetMoreOp) Execute(session *mgo.Session) (Replyable, error) {
- session.SetSocketTimeout(0)
+func (op *GetMoreOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
before := time.Now()
- _, _, data, resultReply, err := mgo.ExecOpWithReply(session, &op.GetMoreOp)
+ _, _, data, resultReply, err := mgo.ExecOpWithReply(socket, &op.GetMoreOp)
after := time.Now()
mgoReply, ok := resultReply.(*mgo.ReplyOp)
diff --git a/src/mongo/gotools/mongoreplay/insert_op.go b/src/mongo/gotools/mongoreplay/insert_op.go
index 37016df7c40..a21b7686a35 100644
--- a/src/mongo/gotools/mongoreplay/insert_op.go
+++ b/src/mongo/gotools/mongoreplay/insert_op.go
@@ -87,11 +87,10 @@ func (op *InsertOp) FromReader(r io.Reader) error {
return nil
}
-// Execute performs the InsertOp on a given session, yielding the reply when
+// Execute performs the InsertOp on a given socket, yielding the reply when
// successful (and an error otherwise).
-func (op *InsertOp) Execute(session *mgo.Session) (Replyable, error) {
- session.SetSocketTimeout(0)
- if err := mgo.ExecOpWithoutReply(session, &op.InsertOp); err != nil {
+func (op *InsertOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
+ if err := mgo.ExecOpWithoutReply(socket, &op.InsertOp); err != nil {
return nil, err
}
diff --git a/src/mongo/gotools/mongoreplay/killcursors_op.go b/src/mongo/gotools/mongoreplay/killcursors_op.go
index f219d5484d0..f0c7773a36d 100644
--- a/src/mongo/gotools/mongoreplay/killcursors_op.go
+++ b/src/mongo/gotools/mongoreplay/killcursors_op.go
@@ -65,9 +65,8 @@ func (op *KillCursorsOp) FromReader(r io.Reader) error {
// Execute performs the KillCursorsOp on a given session, yielding the reply
// when successful (and an error otherwise).
-func (op *KillCursorsOp) Execute(session *mgo.Session) (Replyable, error) {
- session.SetSocketTimeout(0)
- if err := mgo.ExecOpWithoutReply(session, &op.KillCursorsOp); err != nil {
+func (op *KillCursorsOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
+ if err := mgo.ExecOpWithoutReply(socket, &op.KillCursorsOp); err != nil {
return nil, err
}
diff --git a/src/mongo/gotools/mongoreplay/main/mongoreplay.go b/src/mongo/gotools/mongoreplay/main/mongoreplay.go
index 5a7fd02c809..8c89997439b 100644
--- a/src/mongo/gotools/mongoreplay/main/mongoreplay.go
+++ b/src/mongo/gotools/mongoreplay/main/mongoreplay.go
@@ -4,7 +4,9 @@ import (
"github.com/jessevdk/go-flags"
"github.com/mongodb/mongo-tools/mongoreplay"
+ "fmt"
"os"
+ "runtime"
)
const (
@@ -27,6 +29,11 @@ func main() {
os.Exit(ExitOk)
}
+ if runtime.NumCPU() == 1 {
+ fmt.Fprint(os.Stderr, "mongoreplay must be run with multiple threads")
+ os.Exit(ExitError)
+ }
+
opts := mongoreplay.Options{}
var parser = flags.NewParser(&opts, flags.Default)
@@ -49,6 +56,12 @@ func main() {
panic(err)
}
+ _, err = parser.AddCommand("filter", "Filter playback file", "",
+ &mongoreplay.FilterCommand{GlobalOpts: &opts})
+ if err != nil {
+ panic(err)
+ }
+
_, err = parser.Parse()
if err != nil {
diff --git a/src/mongo/gotools/mongoreplay/mongo_op_handler.go b/src/mongo/gotools/mongoreplay/mongo_op_handler.go
index a48a2d9d31a..fc10129fbfd 100644
--- a/src/mongo/gotools/mongoreplay/mongo_op_handler.go
+++ b/src/mongo/gotools/mongoreplay/mongo_op_handler.go
@@ -52,6 +52,7 @@ func (stream *stream) ReassemblyComplete() {
panic("negative openStreamCount")
}
if count == 0 {
+ stream.bidi.handleStreamCompleted()
stream.bidi.close()
}
}
@@ -307,6 +308,22 @@ func (bidi *bidi) handleStreamStateOutOfSync(stream *stream) {
stream.reassembly.Bytes = stream.reassembly.Bytes[:0]
return
}
+func (bidi *bidi) handleStreamCompleted() {
+ var lastOpTimeStamp time.Time
+ if bidi.streams[0].opTimeStamp.After(bidi.streams[1].opTimeStamp) {
+ lastOpTimeStamp = bidi.streams[0].opTimeStamp
+ } else {
+ lastOpTimeStamp = bidi.streams[1].opTimeStamp
+ }
+ if !lastOpTimeStamp.IsZero() {
+ bidi.opStream.unorderedOps <- RecordedOp{
+ Seen: &PreciseTime{lastOpTimeStamp.Add(time.Nanosecond)},
+ SeenConnectionNum: bidi.connectionNumber,
+ EOF: true,
+ }
+ }
+ bidi.logvf(Info, "Connection %v: finishing", bidi.connectionNumber)
+}
// streamOps reads tcpassembly.Reassembly[] blocks from the
// stream's and tries to create whole protocol messages from them.
@@ -323,7 +340,7 @@ func (bidi *bidi) streamOps() {
reassembliesStream = 1
}
if !ok {
- break
+ return
}
stream := bidi.streams[reassembliesStream]
@@ -361,5 +378,4 @@ func (bidi *bidi) streamOps() {
// inform the tcpassembly that we've finished with the reassemblies.
stream.done <- nil
}
- bidi.logvf(Info, "Connection %v: finishing", bidi.connectionNumber)
}
diff --git a/src/mongo/gotools/mongoreplay/mongoreplay.go b/src/mongo/gotools/mongoreplay/mongoreplay.go
index fca27f0701d..ed06cec5014 100644
--- a/src/mongo/gotools/mongoreplay/mongoreplay.go
+++ b/src/mongo/gotools/mongoreplay/mongoreplay.go
@@ -17,6 +17,9 @@ func (opts *Options) SetLogging() {
}
userInfoLogger.setVerbosity(v)
toolDebugLogger.setVerbosity(d)
+ if d > 0 || v > 0 {
+ printVersionInfo()
+ }
}
type VersionOptions struct {
diff --git a/src/mongo/gotools/mongoreplay/monitor.go b/src/mongo/gotools/mongoreplay/monitor.go
index f79578e70c0..f233d610f06 100644
--- a/src/mongo/gotools/mongoreplay/monitor.go
+++ b/src/mongo/gotools/mongoreplay/monitor.go
@@ -98,7 +98,7 @@ func (monitor *MonitorCommand) Execute(args []string) error {
if err != nil {
return err
}
- opChan, errChan = NewOpChanFromFile(playbackFileReader, 1)
+ opChan, errChan = playbackFileReader.OpChan(1)
} else {
ctx, err := getOpstream(monitor.OpStreamSettings)
diff --git a/src/mongo/gotools/mongoreplay/op.go b/src/mongo/gotools/mongoreplay/op.go
index e8cab8ff2f5..5a821234bfa 100644
--- a/src/mongo/gotools/mongoreplay/op.go
+++ b/src/mongo/gotools/mongoreplay/op.go
@@ -46,9 +46,9 @@ type Op interface {
// structure.
FromReader(io.Reader) error
- // Execute performs the op on a given session, yielding the reply when
+ // Execute performs the op on a given socket, yielding the reply when
// successful (and an error otherwise).
- Execute(*mgo.Session) (Replyable, error)
+ Execute(*mgo.MongoSocket) (Replyable, error)
// Meta returns metadata about the operation, useful for analysis of traffic.
Meta() OpMetadata
diff --git a/src/mongo/gotools/mongoreplay/op_msg.go b/src/mongo/gotools/mongoreplay/op_msg.go
index e0d92483591..e2d531aeac6 100644
--- a/src/mongo/gotools/mongoreplay/op_msg.go
+++ b/src/mongo/gotools/mongoreplay/op_msg.go
@@ -25,7 +25,7 @@ func (op *OpMsg) FromReader(r io.Reader) error {
}
// Execute does nothing for an OpMsg
-func (op *OpMsg) Execute(session *mgo.Session) (*ReplyOp, error) {
+func (op *OpMsg) Execute(socket *mgo.MongoSocket) (*ReplyOp, error) {
return nil, nil
}
diff --git a/src/mongo/gotools/mongoreplay/pcap_test.go b/src/mongo/gotools/mongoreplay/pcap_test.go
index 185e9673f1e..40ff4d29e18 100644
--- a/src/mongo/gotools/mongoreplay/pcap_test.go
+++ b/src/mongo/gotools/mongoreplay/pcap_test.go
@@ -1,6 +1,7 @@
package mongoreplay
import (
+ "fmt"
"io"
"os"
"testing"
@@ -126,38 +127,77 @@ func TestMultiChannelGetMoreLiveDB(t *testing.T) {
pcapTestHelper(t, pcapFname, true, verifier)
}
-func pcapTestHelper(t *testing.T, pcapFname string, preprocess bool, verifier verifyFunc) {
+func TestRecordEOF(t *testing.T) {
+ pcapFile := "testPcap/workload_with_EOF.pcap"
- pcapFile := "mongoreplay/testPcap/" + pcapFname
if _, err := os.Stat(pcapFile); err != nil {
t.Skipf("pcap file %v not present, skipping test", pcapFile)
}
- if err := teardownDB(); err != nil {
- t.Error(err)
+ playbackFname := "pcaptest_run.playback"
+ err := playbackFileFromPcap(pcapFile, playbackFname)
+ if err != nil {
+ t.Errorf("error creating playback file from pcap: %v\n", err)
+ }
+
+ playbackReader, err := NewPlaybackFileReader(playbackFname, false)
+ if err != nil {
+ t.Errorf("error opening playback file to write: %v\n", err)
}
- playbackFname := "pcap_test_run.tape"
+ count := 1
+ for {
+ recordedOp, err := playbackReader.NextRecordedOp()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ t.Error(err)
+ }
+ if count == 27 || count == 54 {
+ if !recordedOp.EOF {
+ t.Errorf("expecting EOF op to be placed in recording file")
+ }
+ }
+ count++
+ }
+
+}
+
+func playbackFileFromPcap(pcapFname, playbackFname string) error {
+
streamSettings := OpStreamSettings{
- PcapFile: pcapFile,
+ PcapFile: pcapFname,
PacketBufSize: 9000,
}
- t.Log("Opening op stream")
ctx, err := getOpstream(streamSettings)
if err != nil {
- t.Errorf("error opening opstream: %v\n", err)
+ return fmt.Errorf("couldn't open opstream: %v", err)
}
- playbackWriter, err := NewPlaybackWriter(playbackFname, false)
- defer os.Remove(playbackFname)
+ playbackWriter, err := NewPlaybackFileWriter(playbackFname, false, false)
if err != nil {
- t.Errorf("error opening playback file to write: %v\n", err)
+ return err
}
- t.Log("Recording playbackfile from pcap file")
err = Record(ctx, playbackWriter, false)
if err != nil {
- t.Errorf("error makign tape file: %v\n", err)
+ return err
+ }
+
+ return nil
+}
+
+func pcapTestHelper(t *testing.T, pcapFname string, preprocess bool, verifier verifyFunc) {
+ pcapFile := "mongoreplay/testPcap/" + pcapFname
+ if _, err := os.Stat(pcapFile); err != nil {
+ t.Skipf("pcap file %v not present, skipping test", pcapFile)
+ }
+ playbackFname := "pcap_test_run.playback"
+ err := playbackFileFromPcap(pcapFname, playbackFname)
+ defer os.Remove(playbackFname)
+ if err != nil {
+ t.Errorf("error writing playbackfile %v\n", err)
}
playbackReader, err := NewPlaybackFileReader(playbackFname, false)
@@ -165,13 +205,21 @@ func pcapTestHelper(t *testing.T, pcapFname string, preprocess bool, verifier ve
t.Errorf("error opening playback file to write: %v\n", err)
}
+ if err := teardownDB(); err != nil {
+ t.Error(err)
+ }
+
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
var preprocessMap preprocessCursorManager
if preprocess {
- opChan, errChan := NewOpChanFromFile(playbackReader, 1)
+ opChan, errChan := playbackReader.OpChan(1)
preprocessMap, err := newPreprocessCursorManager(opChan)
if err != nil {
@@ -190,10 +238,10 @@ func pcapTestHelper(t *testing.T, pcapFname string, preprocess bool, verifier ve
context.CursorIDMap = preprocessMap
}
- opChan, errChan := NewOpChanFromFile(playbackReader, 1)
+ opChan, errChan := playbackReader.OpChan(1)
t.Log("Reading ops from playback file")
- err = Play(context, opChan, testSpeed, currentTestURL, 1, 30)
+ err = Play(context, opChan, testSpeed, 1, 30)
if err != nil {
t.Errorf("error playing back recorded file: %v\n", err)
}
diff --git a/src/mongo/gotools/mongoreplay/play.go b/src/mongo/gotools/mongoreplay/play.go
index baee4db33dd..74f0c77ab4a 100644
--- a/src/mongo/gotools/mongoreplay/play.go
+++ b/src/mongo/gotools/mongoreplay/play.go
@@ -1,13 +1,11 @@
package mongoreplay
import (
- "compress/gzip"
"fmt"
"io"
- "os"
"time"
- "github.com/10gen/llmgo/bson"
+ mgo "github.com/10gen/llmgo"
)
// PlayCommand stores settings for the mongoreplay 'play' subcommand
@@ -22,143 +20,11 @@ type PlayCommand struct {
NoPreprocess bool `long:"no-preprocess" description:"don't preprocess the input file to premap data such as mongo cursorIDs"`
Gzip bool `long:"gzip" description:"decompress gzipped input"`
Collect string `long:"collect" description:"Stat collection format; 'format' option uses the --format string" choice:"json" choice:"format" choice:"none" default:"none"`
+ FullSpeed bool `long:"fullSpeed" description:"run the playback as fast as possible"`
}
const queueGranularity = 1000
-// NewOpChanFromFile runs a goroutine that will read and unmarshal recorded ops
-// from a file and push them in to a recorded op chan. Any errors encountered
-// are pushed to an error chan. Both the recorded op chan and the error chan are
-// returned by the function.
-// The error chan won't be readable until the recorded op chan gets closed.
-func NewOpChanFromFile(file *PlaybackFileReader, repeat int) (<-chan *RecordedOp, <-chan error) {
- ch := make(chan *RecordedOp)
- e := make(chan error)
-
- var last time.Time
- var first time.Time
- var loopDelta time.Duration
- go func() {
- defer close(e)
- e <- func() error {
- defer close(ch)
- toolDebugLogger.Logv(Info, "Beginning tapefile read")
- for generation := 0; generation < repeat; generation++ {
- _, err := file.Seek(0, 0)
- if err != nil {
- return fmt.Errorf("PlaybackFile Seek: %v", err)
- }
-
- var order int64
- for {
- recordedOp, err := file.NextRecordedOp()
- if err != nil {
- if err == io.EOF {
- break
- }
- return err
- }
- last = recordedOp.Seen.Time
- if first.IsZero() {
- first = recordedOp.Seen.Time
- }
- recordedOp.Seen.Time = recordedOp.Seen.Add(loopDelta)
- recordedOp.Generation = generation
- recordedOp.Order = order
- // We want to suppress EOF's unless you're in the last
- // generation because all of the ops for one connection
- // across different generations get executed in the same
- // session. We don't want to close the session until the
- // connection closes in the last generation.
- if !recordedOp.EOF || generation == repeat-1 {
- ch <- recordedOp
- }
- order++
- }
- toolDebugLogger.Logvf(DebugHigh, "generation: %v", generation)
- loopDelta += last.Sub(first)
- first = time.Time{}
- continue
- }
- return io.EOF
- }()
- }()
- return ch, e
-}
-
-// GzipReadSeeker wraps an io.ReadSeeker for gzip reading
-type GzipReadSeeker struct {
- readSeeker io.ReadSeeker
- *gzip.Reader
-}
-
-// NewGzipReadSeeker initializes a new GzipReadSeeker
-func NewGzipReadSeeker(rs io.ReadSeeker) (*GzipReadSeeker, error) {
- gzipReader, err := gzip.NewReader(rs)
- if err != nil {
- return nil, err
- }
- return &GzipReadSeeker{rs, gzipReader}, nil
-}
-
-// Seek sets the offset for the next Read, and can only seek to the
-// beginning of the file.
-func (g *GzipReadSeeker) Seek(offset int64, whence int) (int64, error) {
- if whence != 0 || offset != 0 {
- return 0, fmt.Errorf("GzipReadSeeker can only seek to beginning of file")
- }
- _, err := g.readSeeker.Seek(offset, whence)
- if err != nil {
- return 0, err
- }
- g.Reset(g.readSeeker)
- return 0, nil
-}
-
-// PlaybackFileReader stores the necessary information for a playback source,
-// which is just an io.ReadCloser.
-type PlaybackFileReader struct {
- io.ReadSeeker
-}
-
-// NewPlaybackFileReader initializes a new PlaybackFileReader
-func NewPlaybackFileReader(filename string, gzip bool) (*PlaybackFileReader, error) {
- var readSeeker io.ReadSeeker
-
- readSeeker, err := os.Open(filename)
- if err != nil {
- return nil, err
- }
-
- if gzip {
- readSeeker, err = NewGzipReadSeeker(readSeeker)
- if err != nil {
- return nil, err
- }
- }
-
- return &PlaybackFileReader{readSeeker}, nil
-}
-
-// NextRecordedOp iterates through the PlaybackFileReader to yield the next
-// RecordedOp. It returns io.EOF when successfully complete.
-func (file *PlaybackFileReader) NextRecordedOp() (*RecordedOp, error) {
- buf, err := ReadDocument(file)
- if err != nil {
- if err != io.EOF {
- err = fmt.Errorf("ReadDocument Error: %v", err)
- }
- return nil, err
- }
- doc := new(RecordedOp)
- err = bson.Unmarshal(buf, doc)
- if err != nil {
- return nil, fmt.Errorf("Unmarshal RecordedOp Error: %v\n", err)
- }
-
- return doc, nil
-}
-
// ValidateParams validates the settings described in the PlayCommand struct.
func (play *PlayCommand) ValidateParams(args []string) error {
switch {
@@ -184,20 +50,32 @@ func (play *PlayCommand) Execute(args []string) error {
if err != nil {
return err
}
- userInfoLogger.Logvf(Always, "Doing playback at %.2fx speed", play.Speed)
+
+ if play.FullSpeed {
+ userInfoLogger.Logvf(Always, "Doing playback at full speed")
+ } else {
+ userInfoLogger.Logvf(Always, "Doing playback at %.2fx speed", play.Speed)
+ }
playbackFileReader, err := NewPlaybackFileReader(play.PlaybackFile, play.Gzip)
if err != nil {
return err
}
- context := NewExecutionContext(statColl)
+ session, err := mgo.Dial(play.URL)
+ if err != nil {
+ return err
+ }
+ session.SetSocketTimeout(0)
+
+ context := NewExecutionContext(statColl, session, &ExecutionOptions{fullSpeed: play.FullSpeed,
+ driverOpsFiltered: playbackFileReader.metadata.DriverOpsFiltered})
var opChan <-chan *RecordedOp
var errChan <-chan error
if !play.NoPreprocess {
- opChan, errChan = NewOpChanFromFile(playbackFileReader, 1)
+ opChan, errChan = playbackFileReader.OpChan(1)
preprocessMap, err := newPreprocessCursorManager(opChan)
@@ -217,9 +95,9 @@ func (play *PlayCommand) Execute(args []string) error {
context.CursorIDMap = preprocessMap
}
- opChan, errChan = NewOpChanFromFile(playbackFileReader, play.Repeat)
+ opChan, errChan = playbackFileReader.OpChan(play.Repeat)
- if err := Play(context, opChan, play.Speed, play.URL, play.Repeat, play.QueueTime); err != nil {
+ if err := Play(context, opChan, play.Speed, play.Repeat, play.QueueTime); err != nil {
userInfoLogger.Logvf(Always, "Play: %v\n", err)
}
@@ -231,16 +109,14 @@ func (play *PlayCommand) Execute(args []string) error {
return nil
}
-// Play is responsible for playing ops from a RecordedOp channel to the
-// given url.
+// Play is responsible for playing ops from a RecordedOp channel to the session.
func Play(context *ExecutionContext,
opChan <-chan *RecordedOp,
speed float64,
- url string,
repeat int,
queueTime int) error {
- sessionChans := make(map[string]chan<- *RecordedOp)
+ connectionChans := make(map[int64]chan<- *RecordedOp)
var playbackStartTime, recordingStartTime time.Time
var connectionID int64
var opCounter int
@@ -271,38 +147,33 @@ func Play(context *ExecutionContext,
// don't sleep after every read, and generally read and queue
// queueGranularity number of ops at a time and then sleep until the
// last read op is QueueTime ahead.
- if opCounter%queueGranularity == 0 {
- toolDebugLogger.Logvf(DebugHigh, "Waiting to prevent excess buffering with opCounter: %v", opCounter)
- time.Sleep(op.PlayAt.Add(time.Duration(-queueTime) * time.Second).Sub(time.Now()))
+ if !context.fullSpeed {
+ if opCounter%queueGranularity == 0 {
+ toolDebugLogger.Logvf(DebugHigh, "Waiting to prevent excess buffering with opCounter: %v", opCounter)
+ time.Sleep(op.PlayAt.Add(time.Duration(-queueTime) * time.Second).Sub(time.Now()))
+ }
}
- var connectionString string
- if op.OpCode() == OpCodeReply || op.OpCode() == OpCodeCommandReply {
- connectionString = op.ReversedConnectionString()
- } else {
- connectionString = op.ConnectionString()
- }
- sessionChan, ok := sessionChans[connectionString]
+ connectionChan, ok := connectionChans[op.SeenConnectionNum]
if !ok {
connectionID++
- sessionChan = context.newExecutionSession(url, op.PlayAt.Time, connectionID)
- sessionChans[connectionString] = sessionChan
+ connectionChan = context.newExecutionConnection(op.PlayAt.Time, connectionID)
+ connectionChans[op.SeenConnectionNum] = connectionChan
}
if op.EOF {
userInfoLogger.Logv(DebugLow, "EOF Seen in playback")
- close(sessionChan)
- delete(sessionChans, connectionString)
+ close(connectionChan)
+ delete(connectionChans, op.SeenConnectionNum)
} else {
- sessionChan <- op
-
+ connectionChan <- op
}
}
- for connectionString, sessionChan := range sessionChans {
- close(sessionChan)
- delete(sessionChans, connectionString)
+ for connectionNum, connectionChan := range connectionChans {
+ close(connectionChan)
+ delete(connectionChans, connectionNum)
}
- toolDebugLogger.Logvf(Info, "Waiting for sessions to finish")
- context.SessionChansWaitGroup.Wait()
+ toolDebugLogger.Logvf(Info, "Waiting for connections to finish")
+ context.ConnectionChansWaitGroup.Wait()
context.StatCollector.Close()
toolDebugLogger.Logvf(Always, "%v ops played back in %v seconds over %v connections", opCounter, time.Now().Sub(playbackStartTime), connectionID)
diff --git a/src/mongo/gotools/mongoreplay/play_livedb_test.go b/src/mongo/gotools/mongoreplay/play_livedb_test.go
index 3f1dc3473c8..0595491d9d3 100644
--- a/src/mongo/gotools/mongoreplay/play_livedb_test.go
+++ b/src/mongo/gotools/mongoreplay/play_livedb_test.go
@@ -182,11 +182,15 @@ func TestOpInsertLiveDB(t *testing.T) {
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -298,11 +302,15 @@ func TestUpdateOpLiveDB(t *testing.T) {
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -401,11 +409,15 @@ func TestQueryOpLiveDB(t *testing.T) {
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -489,11 +501,15 @@ func TestOpGetMoreLiveDB(t *testing.T) {
}()
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -590,11 +606,15 @@ func TestOpGetMoreMultiCursorLiveDB(t *testing.T) {
}()
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -711,11 +731,15 @@ func TestOpKillCursorsLiveDB(t *testing.T) {
}()
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -777,11 +801,15 @@ func TestCommandOpInsertLiveDB(t *testing.T) {
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -877,11 +905,15 @@ func TestCommandOpFindLiveDB(t *testing.T) {
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
@@ -969,11 +1001,15 @@ func TestCommandOpGetMoreLiveDB(t *testing.T) {
}()
statCollector, _ := newStatCollector(testCollectorOpts, "format", true, true)
statRec := statCollector.StatRecorder.(*BufferedStatRecorder)
- context := NewExecutionContext(statCollector)
+ replaySession, err := mgo.Dial(currentTestURL)
+ if err != nil {
+ t.Errorf("Error connecting to test server: %v", err)
+ }
+ context := NewExecutionContext(statCollector, replaySession, &ExecutionOptions{})
// run mongoreplay's Play loop with the stubbed objects
t.Logf("Beginning mongoreplay playback of generated traffic against host: %v\n", currentTestURL)
- err := Play(context, generator.opChan, testSpeed, currentTestURL, 1, 10)
+ err = Play(context, generator.opChan, testSpeed, 1, 10)
if err != nil {
t.Errorf("Error Playing traffic: %v\n", err)
}
diff --git a/src/mongo/gotools/mongoreplay/play_test.go b/src/mongo/gotools/mongoreplay/play_test.go
index e73efc7a436..19baf76457f 100644
--- a/src/mongo/gotools/mongoreplay/play_test.go
+++ b/src/mongo/gotools/mongoreplay/play_test.go
@@ -5,32 +5,47 @@ import (
"io"
"testing"
"time"
-
- "github.com/10gen/llmgo/bson"
)
func TestRepeatGeneration(t *testing.T) {
recOp := &RecordedOp{
Seen: &PreciseTime{time.Now()},
}
- bsonBytes, err := bson.Marshal(recOp)
+
+ var buf bytes.Buffer
+ wc := NopWriteCloser(&buf)
+ file, err := playbackFileWriterFromWriteCloser(wc, "", PlaybackFileMetadata{})
+ if err != nil {
+ t.Fatalf("error creating playback file %v", err)
+ }
+
+ err = bsonToWriter(file, recOp)
+ if err != nil {
+ t.Fatalf("error writing to bson file %v", err)
+ }
+
+ rs := bytes.NewReader(buf.Bytes())
+ playbackReader, err := playbackFileReaderFromReadSeeker(rs, "")
if err != nil {
- t.Errorf("couldn't marshal %v", err)
+ t.Fatalf("unable to read from playback file %v", err)
}
- playbackReader := &PlaybackFileReader{bytes.NewReader(bsonBytes)}
repeat := 2
- opChan, errChan := NewOpChanFromFile(playbackReader, repeat)
+ opChan, errChan := playbackReader.OpChan(repeat)
op1, ok := <-opChan
if !ok {
- t.Errorf("read of 0-generation op failed")
+ err, ok := <-errChan
+ if ok {
+ t.Logf("error: %v", err)
+ }
+ t.Fatalf("read of 0-generation op failed")
}
if op1.Generation != 0 {
t.Errorf("generation of 0 generation op is %v", op1.Generation)
}
op2, ok := <-opChan
if !ok {
- t.Errorf("read of 1-generation op failed")
+ t.Fatalf("read of 1-generation op failed")
}
if op2.Generation != 1 {
t.Errorf("generation of 1 generation op is %v", op2.Generation)
@@ -53,28 +68,38 @@ func TestPlayOpEOF(t *testing.T) {
EOF: true,
}}
var buf bytes.Buffer
+ wc := NopWriteCloser(&buf)
+ file, err := playbackFileWriterFromWriteCloser(wc, "", PlaybackFileMetadata{})
+ if err != nil {
+ t.Fatalf("error creating playback file %v", err)
+ }
+
for _, op := range ops {
- bsonBytes, err := bson.Marshal(op)
+ err := bsonToWriter(file, op)
if err != nil {
- t.Errorf("couldn't marshal op %v", err)
+ t.Fatalf("unable to write to playback file %v", err)
}
- buf.Write(bsonBytes)
}
- playbackReader := &PlaybackFileReader{bytes.NewReader(buf.Bytes())}
+
+ rs := bytes.NewReader(buf.Bytes())
+ playbackReader, err := playbackFileReaderFromReadSeeker(rs, "")
+ if err != nil {
+ t.Fatalf("unable to read from playback file %v", err)
+ }
repeat := 2
- opChan, errChan := NewOpChanFromFile(playbackReader, repeat)
+ opChan, errChan := playbackReader.OpChan(repeat)
op1, ok := <-opChan
if !ok {
- t.Errorf("read of op1 failed")
+ t.Fatalf("read of op1 failed")
}
if op1.EOF {
t.Errorf("op1 should not be an EOF op")
}
op2, ok := <-opChan
if !ok {
- t.Errorf("read op2 failed")
+ t.Fatalf("read op2 failed")
}
if op2.EOF {
t.Errorf("op2 should not be an EOF op")
@@ -91,7 +116,7 @@ func TestPlayOpEOF(t *testing.T) {
if ok {
t.Errorf("Successfully read past end of op chan")
}
- err := <-errChan
+ err = <-errChan
if err != io.EOF {
t.Errorf("should have eof at end, but got %v", err)
}
diff --git a/src/mongo/gotools/mongoreplay/playbackfile.go b/src/mongo/gotools/mongoreplay/playbackfile.go
new file mode 100644
index 00000000000..16cec477063
--- /dev/null
+++ b/src/mongo/gotools/mongoreplay/playbackfile.go
@@ -0,0 +1,227 @@
+package mongoreplay
+
+import (
+ "compress/gzip"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/10gen/llmgo/bson"
+ "github.com/mongodb/mongo-tools/common/util"
+)
+
+const PlaybackFileVersion = 1
+
+type PlaybackFileMetadata struct {
+ PlaybackFileVersion int
+ DriverOpsFiltered bool
+}
+
+// PlaybackFileReader stores the necessary information for a playback source,
+// which is just an io.ReadCloser.
+type PlaybackFileReader struct {
+ io.ReadSeeker
+ fname string
+
+ metadata PlaybackFileMetadata
+}
+
+// PlaybackFileWriter stores the necessary information for a playback destination,
+// which is an io.WriteCloser and its location.
+type PlaybackFileWriter struct {
+ io.WriteCloser
+ fname string
+
+ metadata PlaybackFileMetadata
+}
+
+// GzipReadSeeker wraps an io.ReadSeeker for gzip reading
+type GzipReadSeeker struct {
+ readSeeker io.ReadSeeker
+ *gzip.Reader
+}
+
+// NewPlaybackFileReader initializes a new PlaybackFileReader
+func NewPlaybackFileReader(filename string, gzip bool) (*PlaybackFileReader, error) {
+ var readSeeker io.ReadSeeker
+
+ readSeeker, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ if gzip {
+ readSeeker, err = NewGzipReadSeeker(readSeeker)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return playbackFileReaderFromReadSeeker(readSeeker, filename)
+}
+
+func playbackFileReaderFromReadSeeker(rs io.ReadSeeker, filename string) (*PlaybackFileReader, error) {
+
+ // read the metadata from the file
+ metadata := new(PlaybackFileMetadata)
+ err := bsonFromReader(rs, metadata)
+ if err != nil {
+ return nil, fmt.Errorf("error reading metadata: %v", err)
+ }
+
+ return &PlaybackFileReader{
+ ReadSeeker: rs,
+ fname: filename,
+
+ metadata: *metadata,
+ }, nil
+}
+
+// NextRecordedOp iterates through the PlaybackFileReader to yield the next
+// RecordedOp. It returns io.EOF when successfully complete.
+func (file *PlaybackFileReader) NextRecordedOp() (*RecordedOp, error) {
+ doc := new(RecordedOp)
+ err := bsonFromReader(file, doc)
+ if err != nil {
+ if err != io.EOF {
+ err = fmt.Errorf("ReadDocument Error: %v", err)
+ }
+ return nil, err
+ }
+ return doc, nil
+}
+
+// NewPlaybackFileWriter initializes a new PlaybackFileWriter
+func NewPlaybackFileWriter(playbackFileName string, driverOpsFiltered, isGzipWriter bool) (*PlaybackFileWriter, error) {
+ metadata := PlaybackFileMetadata{
+ PlaybackFileVersion: PlaybackFileVersion,
+ DriverOpsFiltered: driverOpsFiltered,
+ }
+
+ toolDebugLogger.Logvf(DebugLow, "Opening playback file %v", playbackFileName)
+ file, err := os.Create(playbackFileName)
+ if err != nil {
+ return nil, fmt.Errorf("error opening playback file to write to: %v", err)
+ }
+
+ var wc io.WriteCloser = file
+
+ if isGzipWriter {
+ wc = &util.WrappedWriteCloser{gzip.NewWriter(file), file}
+ }
+
+ return playbackFileWriterFromWriteCloser(wc, playbackFileName, metadata)
+}
+
+func playbackFileWriterFromWriteCloser(wc io.WriteCloser, filename string,
+ metadata PlaybackFileMetadata) (*PlaybackFileWriter, error) {
+
+ bsonBytes, err := bson.Marshal(metadata)
+ if err != nil {
+ return nil, fmt.Errorf("error writing metadata: %v", err)
+ }
+
+ _, err = wc.Write(bsonBytes)
+ if err != nil {
+ return nil, fmt.Errorf("error writing metadata: %v", err)
+ }
+
+ return &PlaybackFileWriter{
+ WriteCloser: wc,
+ fname: filename,
+
+ metadata: metadata,
+ }, nil
+
+}
+
+// NewGzipReadSeeker initializes a new GzipReadSeeker
+func NewGzipReadSeeker(rs io.ReadSeeker) (*GzipReadSeeker, error) {
+ gzipReader, err := gzip.NewReader(rs)
+ if err != nil {
+ return nil, err
+ }
+ return &GzipReadSeeker{rs, gzipReader}, nil
+}
+
+// Seek sets the offset for the next Read, and can only seek to the
+// beginning of the file.
+func (g *GzipReadSeeker) Seek(offset int64, whence int) (int64, error) {
+ if whence != 0 || offset != 0 {
+ return 0, fmt.Errorf("GzipReadSeeker can only seek to beginning of file")
+ }
+ _, err := g.readSeeker.Seek(offset, whence)
+ if err != nil {
+ return 0, err
+ }
+ g.Reset(g.readSeeker)
+ return 0, nil
+}
+
+// OpChan runs a goroutine that will read and unmarshal recorded ops
+// from a file and push them in to a recorded op chan. Any errors encountered
+// are pushed to an error chan. Both the recorded op chan and the error chan are
+// returned by the function.
+// The error chan won't be readable until the recorded op chan gets closed.
+func (pfReader *PlaybackFileReader) OpChan(repeat int) (<-chan *RecordedOp, <-chan error) {
+ ch := make(chan *RecordedOp)
+ e := make(chan error)
+
+ var last time.Time
+ var first time.Time
+ var loopDelta time.Duration
+ go func() {
+ defer close(e)
+ e <- func() error {
+ defer close(ch)
+ toolDebugLogger.Logv(Info, "Beginning playback file read")
+ for generation := 0; generation < repeat; generation++ {
+ _, err := pfReader.Seek(0, 0)
+ if err != nil {
+ return fmt.Errorf("PlaybackFile Seek: %v", err)
+ }
+
+ // Must read the metadata since file was seeked to 0
+ metadata := new(PlaybackFileMetadata)
+ err = bsonFromReader(pfReader, metadata)
+ if err != nil {
+ return fmt.Errorf("bson read error: %v", err)
+ }
+
+ var order int64
+ for {
+ recordedOp, err := pfReader.NextRecordedOp()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ last = recordedOp.Seen.Time
+ if first.IsZero() {
+ first = recordedOp.Seen.Time
+ }
+ recordedOp.Seen.Time = recordedOp.Seen.Add(loopDelta)
+ recordedOp.Generation = generation
+ recordedOp.Order = order
+ // We want to suppress EOF's unless you're in the last
+ // generation because all of the ops for one connection
+ // across different generations get executed in the same
+ // session. We don't want to close the session until the
+ // connection closes in the last generation.
+ if !recordedOp.EOF || generation == repeat-1 {
+ ch <- recordedOp
+ }
+ order++
+ }
+ toolDebugLogger.Logvf(DebugHigh, "generation: %v", generation)
+ loopDelta += last.Sub(first)
+ first = time.Time{}
+ continue
+ }
+ return io.EOF
+ }()
+ }()
+ return ch, e
+}
diff --git a/src/mongo/gotools/mongoreplay/query_op.go b/src/mongo/gotools/mongoreplay/query_op.go
index de3d5078147..65a92f92e97 100644
--- a/src/mongo/gotools/mongoreplay/query_op.go
+++ b/src/mongo/gotools/mongoreplay/query_op.go
@@ -150,12 +150,11 @@ func (op *QueryOp) FromReader(r io.Reader) error {
return nil
}
-// Execute performs the QueryOp on a given session, yielding the reply when
+// Execute performs the QueryOp on a given socket, yielding the reply when
// successful (and an error otherwise).
-func (op *QueryOp) Execute(session *mgo.Session) (Replyable, error) {
- session.SetSocketTimeout(0)
+func (op *QueryOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
before := time.Now()
- _, _, replyData, resultReply, err := mgo.ExecOpWithReply(session, &op.QueryOp)
+ _, _, replyData, resultReply, err := mgo.ExecOpWithReply(socket, &op.QueryOp)
after := time.Now()
if err != nil {
return nil, err
diff --git a/src/mongo/gotools/mongoreplay/record.go b/src/mongo/gotools/mongoreplay/record.go
index 3137488b923..4479408bfb4 100644
--- a/src/mongo/gotools/mongoreplay/record.go
+++ b/src/mongo/gotools/mongoreplay/record.go
@@ -1,16 +1,12 @@
package mongoreplay
import (
- "compress/gzip"
"fmt"
- "io"
"os"
"os/signal"
"syscall"
- "github.com/10gen/llmgo/bson"
"github.com/google/gopacket/pcap"
- "github.com/mongodb/mongo-tools/common/util"
)
// RecordCommand stores settings for the mongoreplay 'record' subcommand
@@ -101,31 +97,6 @@ func getOpstream(cfg OpStreamSettings) (*packetHandlerContext, error) {
return &packetHandlerContext{h, m, pcapHandle}, nil
}
-// PlaybackWriter stores the necessary information for a playback destination,
-// which is an io.WriteCloser and its location.
-type PlaybackWriter struct {
- io.WriteCloser
- fname string
-}
-
-// NewPlaybackWriter initializes a new PlaybackWriter
-func NewPlaybackWriter(playbackFileName string, isGzipWriter bool) (*PlaybackWriter, error) {
- pbWriter := &PlaybackWriter{
- fname: playbackFileName,
- }
- toolDebugLogger.Logvf(DebugLow, "Opening playback file %v", playbackFileName)
- file, err := os.Create(pbWriter.fname)
- if err != nil {
- return nil, fmt.Errorf("error opening playback file to write to: %v", err)
- }
- if isGzipWriter {
- pbWriter.WriteCloser = &util.WrappedWriteCloser{gzip.NewWriter(file), file}
- } else {
- pbWriter.WriteCloser = file
- }
- return pbWriter, nil
-}
-
// ValidateParams validates the settings described in the RecordCommand struct.
func (record *RecordCommand) ValidateParams(args []string) error {
switch {
@@ -168,19 +139,19 @@ func (record *RecordCommand) Execute(args []string) error {
toolDebugLogger.Logvf(Info, "Got signal %v, closing PCAP handle", s)
ctx.packetHandler.Close()
}()
- playbackWriter, err := NewPlaybackWriter(record.PlaybackFile, record.Gzip)
+ playbackFileWriter, err := NewPlaybackFileWriter(record.PlaybackFile, false, record.Gzip)
if err != nil {
return err
}
- defer playbackWriter.Close()
+ defer playbackFileWriter.Close()
- return Record(ctx, playbackWriter, record.FullReplies)
+ return Record(ctx, playbackFileWriter, record.FullReplies)
}
// Record writes pcap data into a playback file
func Record(ctx *packetHandlerContext,
- playbackWriter *PlaybackWriter,
+ playbackWriter *PlaybackFileWriter,
noShortenReply bool) error {
ch := make(chan error)
@@ -202,12 +173,7 @@ func Record(ctx *packetHandlerContext,
continue
}
}
- bsonBytes, err := bson.Marshal(op)
- if err != nil {
- userInfoLogger.Logvf(DebugLow, "stream %v error marshaling message: %v", op.SeenConnectionNum, err)
- continue
- }
- _, err = playbackWriter.Write(bsonBytes)
+ err := bsonToWriter(playbackWriter, op)
if err != nil {
fail = fmt.Errorf("error writing message: %v", err)
userInfoLogger.Logvf(Always, "%v", err)
diff --git a/src/mongo/gotools/mongoreplay/reply_op.go b/src/mongo/gotools/mongoreplay/reply_op.go
index ac478ed86fb..40ade73c402 100644
--- a/src/mongo/gotools/mongoreplay/reply_op.go
+++ b/src/mongo/gotools/mongoreplay/reply_op.go
@@ -96,9 +96,9 @@ func (op *ReplyOp) FromReader(r io.Reader) error {
return nil
}
-// Execute performs the ReplyOp on a given session, yielding the reply when
+// Execute performs the ReplyOp on a given socket, yielding the reply when
// successful (and an error otherwise).
-func (op *ReplyOp) Execute(session *mgo.Session) (Replyable, error) {
+func (op *ReplyOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
return nil, nil
}
diff --git a/src/mongo/gotools/mongoreplay/stat_collector.go b/src/mongo/gotools/mongoreplay/stat_collector.go
index 91009fe060e..93ce88b9098 100644
--- a/src/mongo/gotools/mongoreplay/stat_collector.go
+++ b/src/mongo/gotools/mongoreplay/stat_collector.go
@@ -32,8 +32,8 @@ type StatOptions struct {
// recording functions
type StatCollector struct {
sync.Once
- done chan struct{}
- statStream chan *OpStat
+ done chan struct{}
+ statStream chan *OpStat
statStreamSize int
StatGenerator
StatRecorder
@@ -138,10 +138,15 @@ func FindValueByKey(keyName string, document *bson.D) (interface{}, bool) {
return nil, false
}
-func shouldCollectOp(op Op) bool {
+func shouldCollectOp(op Op, driverOpsFiltered bool) bool {
_, isReplyOp := op.(*ReplyOp)
_, isCommandReplyOp := op.(*CommandReplyOp)
- return !isReplyOp && !isCommandReplyOp && !IsDriverOp(op)
+
+ var isDriverOp bool
+ if !driverOpsFiltered {
+ isDriverOp = IsDriverOp(op)
+ }
+ return !isReplyOp && !isCommandReplyOp && !isDriverOp
}
// Collect formats the operation statistics as specified by the contained StatGenerator and writes it to
diff --git a/src/mongo/gotools/mongoreplay/testPcap/make_WorkloadWithEOF.js b/src/mongo/gotools/mongoreplay/testPcap/make_WorkloadWithEOF.js
new file mode 100644
index 00000000000..c32aeb7de45
--- /dev/null
+++ b/src/mongo/gotools/mongoreplay/testPcap/make_WorkloadWithEOF.js
@@ -0,0 +1,13 @@
+var db1 = (new Mongo("mongodb://localhost:20000")).getDB("mongoreplay");
+
+db1.test.insert({dummy:true});
+for(var i =0; i < 10; i ++ ){
+ db1.test.find().limit(10).toArray()
+}
+
+db1= (new Mongo("mongodb://localhost:20000")).getDB("mongoreplay");
+
+db1.test.insert({dummy:true});
+for(var i =0; i < 10; i ++ ){
+ db1.test.find().limit(10).toArray()
+}
diff --git a/src/mongo/gotools/mongoreplay/update_op.go b/src/mongo/gotools/mongoreplay/update_op.go
index 90f12d9aa79..cd7aaad1641 100644
--- a/src/mongo/gotools/mongoreplay/update_op.go
+++ b/src/mongo/gotools/mongoreplay/update_op.go
@@ -113,10 +113,10 @@ func (op *UpdateOp) FromReader(r io.Reader) error {
return nil
}
-// Execute performs the UpdateOp on a given session, yielding the reply when
+// Execute performs the UpdateOp on a given socket, yielding the reply when
// successful (and an error otherwise).
-func (op *UpdateOp) Execute(session *mgo.Session) (Replyable, error) {
- if err := mgo.ExecOpWithoutReply(session, &op.UpdateOp); err != nil {
+func (op *UpdateOp) Execute(socket *mgo.MongoSocket) (Replyable, error) {
+ if err := mgo.ExecOpWithoutReply(socket, &op.UpdateOp); err != nil {
return nil, err
}
return nil, nil
diff --git a/src/mongo/gotools/mongoreplay/util.go b/src/mongo/gotools/mongoreplay/util.go
index 8708cb58ecc..c163ebad603 100644
--- a/src/mongo/gotools/mongoreplay/util.go
+++ b/src/mongo/gotools/mongoreplay/util.go
@@ -429,6 +429,35 @@ func (b *PreciseTime) SetBSON(raw bson.Raw) error {
return nil
}
+// bsonFromReader reads a bson document from the reader into out.
+func bsonFromReader(reader io.Reader, out interface{}) error {
+ buf, err := ReadDocument(reader)
+ if err != nil {
+ if err != io.EOF {
+ err = fmt.Errorf("ReadDocument Error: %v", err)
+ }
+ return err
+ }
+ err = bson.Unmarshal(buf, out)
+ if err != nil {
+ return fmt.Errorf("Unmarshal RecordedOp Error: %v\n", err)
+ }
+ return nil
+}
+
+// bsonToWriter writes a bson document to the writer given.
+func bsonToWriter(writer io.Writer, in interface{}) error {
+ bsonBytes, err := bson.Marshal(in)
+ if err != nil {
+ return err
+ }
+ _, err = writer.Write(bsonBytes)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
// bufferWaiter is a channel-like structure which only recieves a buffer
// from its channel once on the first Get() call, then yields the same
// buffer upon subsequent Get() calls.
diff --git a/src/mongo/gotools/mongoreplay/version.go b/src/mongo/gotools/mongoreplay/version.go
index 8d147b8e1ff..26b8b1905b5 100644
--- a/src/mongo/gotools/mongoreplay/version.go
+++ b/src/mongo/gotools/mongoreplay/version.go
@@ -10,12 +10,16 @@ import (
// is specified.
func (o *VersionOptions) PrintVersion() bool {
if o.Version {
- fmt.Printf("%v version: %v\n", "mongoreplay", options.VersionStr)
- fmt.Printf("git version: %v\n", options.Gitspec)
- fmt.Printf("Go version: %v\n", runtime.Version())
- fmt.Printf(" os: %v\n", runtime.GOOS)
- fmt.Printf(" arch: %v\n", runtime.GOARCH)
- fmt.Printf(" compiler: %v\n", runtime.Compiler)
+ printVersionInfo()
}
return o.Version
}
+
+func printVersionInfo() {
+ fmt.Printf("%v version: %v\n", "mongoreplay", options.VersionStr)
+ fmt.Printf("git version: %v\n", options.Gitspec)
+ fmt.Printf("Go version: %v\n", runtime.Version())
+ fmt.Printf(" os: %v\n", runtime.GOOS)
+ fmt.Printf(" arch: %v\n", runtime.GOARCH)
+ fmt.Printf(" compiler: %v\n", runtime.Compiler)
+}
diff --git a/src/mongo/gotools/mongorestore/main/mongorestore.go b/src/mongo/gotools/mongorestore/main/mongorestore.go
index fa6b6d84caa..d5832bcc51d 100644
--- a/src/mongo/gotools/mongorestore/main/mongorestore.go
+++ b/src/mongo/gotools/mongorestore/main/mongorestore.go
@@ -23,15 +23,16 @@ const (
func main() {
// initialize command-line opts
opts := options.New("mongorestore", mongorestore.Usage,
- options.EnabledOptions{Auth: true, Connection: true})
+ options.EnabledOptions{Auth: true, Connection: true, URI: true})
nsOpts := &mongorestore.NSOptions{}
opts.AddOptions(nsOpts)
inputOpts := &mongorestore.InputOptions{}
opts.AddOptions(inputOpts)
outputOpts := &mongorestore.OutputOptions{}
opts.AddOptions(outputOpts)
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsWriteConcern)
- extraArgs, err := opts.Parse()
+ extraArgs, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongorestore --help' for more information")
@@ -53,6 +54,9 @@ func main() {
log.SetVerbosity(opts.Verbosity)
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
+
targetDir, err := getTargetDirFromArgs(extraArgs, inputOpts.Directory)
if err != nil {
log.Logvf(log.Always, "%v", err)
@@ -61,11 +65,6 @@ func main() {
}
targetDir = util.ToUniversalPath(targetDir)
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
-
provider, err := db.NewSessionProvider(*opts)
if err != nil {
log.Logvf(log.Always, "error connecting to host: %v", err)
diff --git a/src/mongo/gotools/mongorestore/mongorestore.go b/src/mongo/gotools/mongorestore/mongorestore.go
index ee6cf6a5599..573354cd8ec 100644
--- a/src/mongo/gotools/mongorestore/mongorestore.go
+++ b/src/mongo/gotools/mongorestore/mongorestore.go
@@ -140,7 +140,8 @@ func (restore *MongoRestore) ParseAndValidateOptions() error {
}
log.Logvf(log.DebugLow, "connected to node type: %v", nodeType)
- restore.safety, err = db.BuildWriteConcern(restore.OutputOptions.WriteConcern, nodeType)
+ restore.safety, err = db.BuildWriteConcern(restore.OutputOptions.WriteConcern, nodeType,
+ restore.ToolOptions.URI.ParsedConnString())
if err != nil {
return fmt.Errorf("error parsing write concern: %v", err)
}
@@ -252,13 +253,15 @@ func (restore *MongoRestore) Restore() error {
}
if restore.InputOptions.Archive != "" {
- archiveReader, err := restore.getArchiveReader()
- if err != nil {
- return err
- }
- restore.archive = &archive.Reader{
- In: archiveReader,
- Prelude: &archive.Prelude{},
+ if restore.archive == nil {
+ archiveReader, err := restore.getArchiveReader()
+ if err != nil {
+ return err
+ }
+ restore.archive = &archive.Reader{
+ In: archiveReader,
+ Prelude: &archive.Prelude{},
+ }
}
err = restore.archive.Prelude.Read(restore.archive.In)
if err != nil {
@@ -320,9 +323,7 @@ func (restore *MongoRestore) Restore() error {
// Create the demux before intent creation, because muted archive intents need
// to register themselves with the demux directly
if restore.InputOptions.Archive != "" {
- restore.archive.Demux = &archive.Demultiplexer{
- In: restore.archive.In,
- }
+ restore.archive.Demux = archive.CreateDemux(restore.archive.Prelude.NamespaceMetadatas, restore.archive.In)
}
switch {
@@ -390,13 +391,18 @@ func (restore *MongoRestore) Restore() error {
return nil
}
+ demuxFinished := make(chan interface{})
+ var demuxErr error
if restore.InputOptions.Archive != "" {
namespaceChan := make(chan string, 1)
namespaceErrorChan := make(chan error)
restore.archive.Demux.NamespaceChan = namespaceChan
restore.archive.Demux.NamespaceErrorChan = namespaceErrorChan
- go restore.archive.Demux.Run()
+ go func() {
+ demuxErr = restore.archive.Demux.Run()
+ close(demuxFinished)
+ }()
// consume the new namespace announcement from the demux for all of the special collections
// that get cached when being read out of the archive.
// The first regular collection found gets pushed back on to the namespaceChan
@@ -485,7 +491,12 @@ func (restore *MongoRestore) Restore() error {
}
}
- log.Logv(log.Always, "done")
+ defer log.Logv(log.Always, "done")
+
+ if restore.InputOptions.Archive != "" {
+ <-demuxFinished
+ return demuxErr
+ }
return nil
}
diff --git a/src/mongo/gotools/mongorestore/mongorestore_archive_test.go b/src/mongo/gotools/mongorestore/mongorestore_archive_test.go
new file mode 100644
index 00000000000..8398e749854
--- /dev/null
+++ b/src/mongo/gotools/mongorestore/mongorestore_archive_test.go
@@ -0,0 +1,96 @@
+package mongorestore
+
+import (
+ "github.com/mongodb/mongo-tools/common/archive"
+ "github.com/mongodb/mongo-tools/common/db"
+ "github.com/mongodb/mongo-tools/common/log"
+ "github.com/mongodb/mongo-tools/common/options"
+ "github.com/mongodb/mongo-tools/common/testutil"
+ "github.com/mongodb/mongo-tools/common/util"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ "io"
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func init() {
+ // bump up the verbosity to make checking debug log output possible
+ log.SetVerbosity(&options.Verbosity{
+ VLevel: 4,
+ })
+}
+
+var (
+ testArchive = "testdata/test.bar.archive"
+)
+
+func TestMongorestoreShortArchive(t *testing.T) {
+ Convey("With a test MongoRestore", t, func() {
+ ssl := testutil.GetSSLOptions()
+ auth := testutil.GetAuthOptions()
+
+ testutil.VerifyTestType(t, testutil.IntegrationTestType)
+ toolOptions := &options.ToolOptions{
+ Connection: &options.Connection{
+ Host: testServer,
+ Port: testPort,
+ },
+ URI: &options.URI{},
+ Auth: &auth,
+ SSL: &ssl,
+ }
+ inputOptions := &InputOptions{
+ Archive: testArchive,
+ }
+ outputOptions := &OutputOptions{
+ NumParallelCollections: 1,
+ NumInsertionWorkers: 1,
+ WriteConcern: "majority",
+ Drop: true,
+ }
+ nsOptions := &NSOptions{}
+ provider, err := db.NewSessionProvider(*toolOptions)
+ if err != nil {
+ log.Logvf(log.Always, "error connecting to host: %v", err)
+ os.Exit(util.ExitError)
+ }
+ file, err := os.Open(testArchive)
+ So(file, ShouldNotBeNil)
+ So(err, ShouldBeNil)
+
+ fi, err := file.Stat()
+ So(fi, ShouldNotBeNil)
+ So(err, ShouldBeNil)
+
+ fileSize := fi.Size()
+
+ for i := fileSize; i >= 0; i-- {
+
+ log.Logvf(log.Always, "Restoring from the first %v bytes of a archive of size %v", i, fileSize)
+
+ _, err = file.Seek(0, 0)
+ So(err, ShouldBeNil)
+
+ restore := MongoRestore{
+ ToolOptions: toolOptions,
+ OutputOptions: outputOptions,
+ InputOptions: inputOptions,
+ NSOptions: nsOptions,
+ SessionProvider: provider,
+ archive: &archive.Reader{
+ Prelude: &archive.Prelude{},
+ In: ioutil.NopCloser(io.LimitReader(file, i)),
+ },
+ }
+ err = restore.Restore()
+ if i == fileSize {
+ So(err, ShouldBeNil)
+ } else {
+ So(err, ShouldNotBeNil)
+ }
+ }
+ })
+}
diff --git a/src/mongo/gotools/mongorestore/mongorestore_test.go b/src/mongo/gotools/mongorestore/mongorestore_test.go
index c943dad6bde..9edb7194daa 100644
--- a/src/mongo/gotools/mongorestore/mongorestore_test.go
+++ b/src/mongo/gotools/mongorestore/mongorestore_test.go
@@ -35,6 +35,7 @@ func TestMongorestore(t *testing.T) {
Host: testServer,
Port: testPort,
},
+ URI: &options.URI{},
Auth: &auth,
SSL: &ssl,
}
diff --git a/src/mongo/gotools/mongorestore/options.go b/src/mongo/gotools/mongorestore/options.go
index 93965cc3e2b..0913cf52e21 100644
--- a/src/mongo/gotools/mongorestore/options.go
+++ b/src/mongo/gotools/mongorestore/options.go
@@ -29,9 +29,12 @@ func (*InputOptions) Name() string {
// OutputOptions defines the set of options for restoring dump data.
type OutputOptions struct {
- Drop bool `long:"drop" description:"drop each collection before import"`
- DryRun bool `long:"dryRun" description:"view summary without importing anything. recommended with verbosity"`
- WriteConcern string `long:"writeConcern" value-name:"<write-concern>" default:"majority" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}' (defaults to 'majority')"`
+ Drop bool `long:"drop" description:"drop each collection before import"`
+ DryRun bool `long:"dryRun" description:"view summary without importing anything. recommended with verbosity"`
+
+ // By default mongorestore uses a write concern of 'majority'.
+ // Cannot be used simultaneously with write concern options in a URI.
+ WriteConcern string `long:"writeConcern" value-name:"<write-concern>" default-mask:"-" description:"write concern options e.g. --writeConcern majority, --writeConcern '{w: 3, wtimeout: 500, fsync: true, j: true}'"`
NoIndexRestore bool `long:"noIndexRestore" description:"don't restore indexes"`
NoOptionsRestore bool `long:"noOptionsRestore" description:"don't restore collection options"`
KeepIndexVersion bool `long:"keepIndexVersion" description:"don't update index version"`
diff --git a/src/mongo/gotools/mongorestore/options_test.go b/src/mongo/gotools/mongorestore/options_test.go
new file mode 100644
index 00000000000..5513ecf2f02
--- /dev/null
+++ b/src/mongo/gotools/mongorestore/options_test.go
@@ -0,0 +1,74 @@
+package mongorestore
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/mongodb/mongo-tools/common/db"
+ "github.com/mongodb/mongo-tools/common/options"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+// Regression test for TOOLS-1741
+func TestWriteConcernWithURIParsing(t *testing.T) {
+ Convey("With an IngestOptions and ToolsOptions", t, func() {
+
+ // create an 'EnabledOptions' to determine what options should be able to be
+ // parsed and set form the input.
+ enabled := options.EnabledOptions{URI: true}
+
+ // create a new tools options to hold the parsed options
+ opts := options.New("", "", enabled)
+
+ // create a 'OutputOptions', which holds the value of the write concern
+ // for mongorestore.
+ outputOpts := &OutputOptions{}
+ opts.AddOptions(outputOpts)
+
+ // Specify that a write concern set on the URI is not an error and is a known
+ // possible option.
+ opts.URI.AddKnownURIParameters(options.KnownURIOptionsWriteConcern)
+
+ Convey("Parsing with no value should leave write concern empty", func() {
+ _, err := opts.ParseArgs([]string{})
+ So(err, ShouldBeNil)
+ So(outputOpts.WriteConcern, ShouldEqual, "")
+ Convey("and building write concern object, WMode should be majority", func() {
+ sessionSafety, err := db.BuildWriteConcern(outputOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldBeNil)
+ So(sessionSafety.WMode, ShouldEqual, "majority")
+ })
+ })
+
+ Convey("Parsing with no writeconcern in URI should not error", func() {
+ args := []string{
+ "--uri", "mongodb://localhost:27017/test",
+ }
+ _, err := opts.ParseArgs(args)
+ So(err, ShouldBeNil)
+ So(outputOpts.WriteConcern, ShouldEqual, "")
+ Convey("and parsing write concern, WMode should be majority", func() {
+ sessionSafety, err := db.BuildWriteConcern(outputOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldBeNil)
+ So(sessionSafety, ShouldNotBeNil)
+ So(sessionSafety.WMode, ShouldEqual, "majority")
+ })
+ })
+ Convey("Parsing with both writeconcern in URI and command line should error", func() {
+ args := []string{
+ "--uri", "mongodb://localhost:27017/test",
+ "--writeConcern", "majority",
+ }
+ _, err := opts.ParseArgs(args)
+ So(err, ShouldBeNil)
+ So(outputOpts.WriteConcern, ShouldEqual, "majority")
+ Convey("and parsing write concern, WMode should be majority", func() {
+ _, err := db.BuildWriteConcern(outputOpts.WriteConcern, "",
+ opts.ParsedConnString())
+ So(err, ShouldResemble, fmt.Errorf("cannot specify writeConcern string and connectionString object"))
+ })
+ })
+ })
+}
diff --git a/src/mongo/gotools/mongorestore/testdata/test.bar.archive b/src/mongo/gotools/mongorestore/testdata/test.bar.archive
new file mode 100644
index 00000000000..50f8f1c877f
--- /dev/null
+++ b/src/mongo/gotools/mongorestore/testdata/test.bar.archive
Binary files differ
diff --git a/src/mongo/gotools/mongostat/main/mongostat.go b/src/mongo/gotools/mongostat/main/mongostat.go
index 8663248ceb6..d6175d03d0f 100644
--- a/src/mongo/gotools/mongostat/main/mongostat.go
+++ b/src/mongo/gotools/mongostat/main/mongostat.go
@@ -50,7 +50,7 @@ func main() {
opts := options.New(
"mongostat",
mongostat.Usage,
- options.EnabledOptions{Connection: true, Auth: true, Namespace: false})
+ options.EnabledOptions{Connection: true, Auth: true, Namespace: false, URI: true})
opts.UseReadOnlyHostDescription()
// add mongostat-specific options
@@ -64,7 +64,7 @@ func main() {
interactiveOption.ShortName = 0
}
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongostat --help' for more information")
@@ -102,7 +102,16 @@ func main() {
return
}
- if opts.Auth.Username != "" && opts.Auth.Source == "" && !opts.Auth.RequiresExternalDB() {
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
+
+ if opts.Auth.Username != "" && opts.GetAuthenticationDatabase() == "" && !opts.Auth.RequiresExternalDB() {
+ // add logic to have different error if using uri
+ if opts.URI != nil && opts.URI.ConnectionString != "" {
+ log.Logvf(log.Always, "authSource is required when authenticating against a non $external database")
+ os.Exit(util.ExitBadOptions)
+ }
+
log.Logvf(log.Always, "--authenticationDatabase is required when authenticating against a non $external database")
os.Exit(util.ExitBadOptions)
}
@@ -212,8 +221,6 @@ func main() {
}
opts.Direct = true
- _, setName := util.ParseConnectionString(opts.Host)
- opts.ReplicaSetName = setName
stat := &mongostat.MongoStat{
Options: opts,
StatOptions: statOpts,
@@ -229,6 +236,7 @@ func main() {
// kick it off
err = stat.Run()
+ formatter.Finish()
if err != nil {
log.Logvf(log.Always, "Failed: %v", err)
os.Exit(util.ExitError)
diff --git a/src/mongo/gotools/mongostat/stat_consumer/formatter.go b/src/mongo/gotools/mongostat/stat_consumer/formatter.go
index 2fb1a4c5506..1acaa842e09 100644
--- a/src/mongo/gotools/mongostat/stat_consumer/formatter.go
+++ b/src/mongo/gotools/mongostat/stat_consumer/formatter.go
@@ -13,6 +13,8 @@ type LineFormatter interface {
// IsFinished returns true iff the formatter cannot print any more data
IsFinished() bool
+ // Finish() is called whem mongostat is shutting down so that the fomatter can clean up
+ Finish()
}
type limitableFormatter struct {
diff --git a/src/mongo/gotools/mongostat/stat_consumer/grid_line_formatter.go b/src/mongo/gotools/mongostat/stat_consumer/grid_line_formatter.go
index 53a6e2d4a37..7a03a21118c 100644
--- a/src/mongo/gotools/mongostat/stat_consumer/grid_line_formatter.go
+++ b/src/mongo/gotools/mongostat/stat_consumer/grid_line_formatter.go
@@ -40,6 +40,9 @@ func init() {
// headerInterval is the number of chunks before the header is re-printed in GridLineFormatter
const headerInterval = 10
+func (glf *GridLineFormatter) Finish() {
+}
+
// FormatLines formats the StatLines as a grid
func (glf *GridLineFormatter) FormatLines(lines []*line.StatLine, headerKeys []string, keyNames map[string]string) string {
buf := &bytes.Buffer{}
diff --git a/src/mongo/gotools/mongostat/stat_consumer/interactive_line_formatter.go b/src/mongo/gotools/mongostat/stat_consumer/interactive_line_formatter.go
index 87cc08fef26..466e7c93d83 100644
--- a/src/mongo/gotools/mongostat/stat_consumer/interactive_line_formatter.go
+++ b/src/mongo/gotools/mongostat/stat_consumer/interactive_line_formatter.go
@@ -6,6 +6,7 @@ import (
"fmt"
"sort"
"strings"
+ "sync"
"github.com/mongodb/mongo-tools/mongostat/stat_consumer/line"
"github.com/nsf/termbox-go"
@@ -19,6 +20,7 @@ type InteractiveLineFormatter struct {
table []*column
row, col int
showHelp bool
+ sync.Mutex
}
func NewInteractiveLineFormatter(_ int64, includeHeader bool) LineFormatter {
@@ -30,6 +32,7 @@ func NewInteractiveLineFormatter(_ int64, includeHeader bool) LineFormatter {
fmt.Printf("Error setting up terminal UI: %v", err)
panic("could not set up interactive terminal interface")
}
+
go func() {
for {
ilf.handleEvent(termbox.PollEvent())
@@ -56,8 +59,15 @@ type cell struct {
header bool
}
+func (ilf *InteractiveLineFormatter) Finish() {
+ termbox.Close()
+}
+
// FormatLines formats the StatLines as a table in the terminal ui
func (ilf *InteractiveLineFormatter) FormatLines(lines []*line.StatLine, headerKeys []string, keyNames map[string]string) string {
+ defer ilf.update() // so that it runs after the unlock, bnecause update locks again
+ ilf.Lock()
+ defer ilf.Unlock()
// keep ordering consistent
sort.Sort(line.StatLines(lines))
@@ -99,14 +109,16 @@ func (ilf *InteractiveLineFormatter) FormatLines(lines []*line.StatLine, headerK
}
}
- ilf.update()
return ""
}
func (ilf *InteractiveLineFormatter) handleEvent(ev termbox.Event) {
+ ilf.Lock()
+ defer ilf.Unlock()
if ev.Type != termbox.EventKey {
return
}
+
currSelected := ilf.table[ilf.col].cells[ilf.row].selected
switch {
case ev.Key == termbox.KeyCtrlC:
@@ -114,7 +126,6 @@ func (ilf *InteractiveLineFormatter) handleEvent(ev termbox.Event) {
case ev.Key == termbox.KeyEsc:
fallthrough
case ev.Ch == 'q':
- termbox.Close()
// our max rowCount is set to 1; increment to exit
ilf.increment()
case ev.Key == termbox.KeyArrowRight:
@@ -190,6 +201,8 @@ func writeString(x, y int, text string, fg, bg termbox.Attribute) {
}
func (ilf *InteractiveLineFormatter) update() {
+ ilf.Lock()
+ defer ilf.Unlock()
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
x := 0
for i, column := range ilf.table {
diff --git a/src/mongo/gotools/mongostat/stat_consumer/json_line_formatter.go b/src/mongo/gotools/mongostat/stat_consumer/json_line_formatter.go
index 0452072ad39..0e214f73e02 100644
--- a/src/mongo/gotools/mongostat/stat_consumer/json_line_formatter.go
+++ b/src/mongo/gotools/mongostat/stat_consumer/json_line_formatter.go
@@ -21,6 +21,8 @@ func NewJSONLineFormatter(maxRows int64, _ bool) LineFormatter {
func init() {
FormatterConstructors["json"] = NewJSONLineFormatter
}
+func (glf *JSONLineFormatter) Finish() {
+}
// FormatLines formats the StatLines as JSON
func (jlf *JSONLineFormatter) FormatLines(lines []*line.StatLine, headerKeys []string, keyNames map[string]string) string {
diff --git a/src/mongo/gotools/mongotop/main/mongotop.go b/src/mongo/gotools/mongotop/main/mongotop.go
index fe06f0e0d64..6a4317dbc8f 100644
--- a/src/mongo/gotools/mongotop/main/mongotop.go
+++ b/src/mongo/gotools/mongotop/main/mongotop.go
@@ -17,14 +17,14 @@ import (
func main() {
// initialize command-line opts
opts := options.New("mongotop", mongotop.Usage,
- options.EnabledOptions{Auth: true, Connection: true, Namespace: false})
+ options.EnabledOptions{Auth: true, Connection: true, Namespace: false, URI: true})
opts.UseReadOnlyHostDescription()
// add mongotop-specific options
outputOpts := &mongotop.Output{}
opts.AddOptions(outputOpts)
- args, err := opts.Parse()
+ args, err := opts.ParseArgs(os.Args[1:])
if err != nil {
log.Logvf(log.Always, "error parsing command line options: %v", err)
log.Logvf(log.Always, "try 'mongotop --help' for more information")
@@ -44,6 +44,9 @@ func main() {
log.SetVerbosity(opts.Verbosity)
signals.Handle()
+ // verify uri options and log them
+ opts.URI.LogUnsupportedOptions()
+
if len(args) > 1 {
log.Logvf(log.Always, "too many positional arguments")
log.Logvf(log.Always, "try 'mongotop --help' for more information")
@@ -64,15 +67,14 @@ func main() {
}
if opts.Auth.Username != "" && opts.Auth.Source == "" && !opts.Auth.RequiresExternalDB() {
+ if opts.URI != nil && opts.URI.ConnectionString != "" {
+ log.Logvf(log.Always, "authSource is required when authenticating against a non $external database")
+ os.Exit(util.ExitBadOptions)
+ }
log.Logvf(log.Always, "--authenticationDatabase is required when authenticating against a non $external database")
os.Exit(util.ExitBadOptions)
}
- // connect directly, unless a replica set name is explicitly specified
- _, setName := util.ParseConnectionString(opts.Host)
- opts.Direct = (setName == "")
- opts.ReplicaSetName = setName
-
// create a session provider to connect to the db
sessionProvider, err := db.NewSessionProvider(*opts)
if err != nil {
@@ -80,7 +82,7 @@ func main() {
os.Exit(util.ExitError)
}
- if setName == "" {
+ if opts.ReplicaSetName == "" {
sessionProvider.SetReadPreference(mgo.PrimaryPreferred)
}
diff --git a/src/mongo/gotools/test.sh b/src/mongo/gotools/test.sh
new file mode 100755
index 00000000000..e30ea3debdc
--- /dev/null
+++ b/src/mongo/gotools/test.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+set -o errexit
+tags=""
+if [ ! -z "$1" ]
+ then
+ tags="$@"
+fi
+
+# make sure we're in the directory where the script lives
+SCRIPT_DIR="$(cd "$(dirname ${BASH_SOURCE[0]})" && pwd)"
+cd $SCRIPT_DIR
+
+sed -i.bak -e "s/built-without-version-string/$(git describe)/" \
+ -e "s/built-without-git-spec/$(git rev-parse HEAD)/" \
+ common/options/options.go
+
+# remove stale packages
+rm -rf vendor/pkg
+
+. ./set_gopath.sh
+mkdir -p bin
+
+for i in bsondump mongostat mongofiles mongoexport mongoimport mongorestore mongodump mongotop mongooplog mongoreplay; do
+ echo "Testing ${i}..."
+ (cd $i && go test)
+done
+
+mv -f common/options/options.go.bak common/options/options.go
diff --git a/src/mongo/gotools/vendor/src/github.com/10gen/llmgo/session.go b/src/mongo/gotools/vendor/src/github.com/10gen/llmgo/session.go
index e06de728352..f2431dc9572 100644
--- a/src/mongo/gotools/vendor/src/github.com/10gen/llmgo/session.go
+++ b/src/mongo/gotools/vendor/src/github.com/10gen/llmgo/session.go
@@ -643,17 +643,12 @@ func (db *Database) Run(cmd interface{}, result interface{}) error {
return err
}
-func ExecOpWithReply(s MongoSession, op OpWithReply) (m []byte, c []byte, data [][]byte, reply interface{}, err error) {
+func ExecOpWithReply(socket *MongoSocket, op OpWithReply) (m []byte, c []byte, data [][]byte, reply interface{}, err error) {
var wait sync.Mutex
var metaData []byte
var commandData []byte
var replyData [][]byte
var replyErr error
- socket, err := s.AcquireSocketPrivate(true)
- if err != nil {
- return nil, nil, nil, nil, err
- }
- defer socket.Release()
wait.Lock()
@@ -705,14 +700,8 @@ func ExecOpWithReply(s MongoSession, op OpWithReply) (m []byte, c []byte, data [
return metaData, commandData, replyData, reply, replyErr
}
-func ExecOpWithoutReply(s MongoSession, op interface{}) error {
- socket, err := s.AcquireSocketPrivate(true)
- if err != nil {
- return err
- }
- defer socket.Release()
-
- err = socket.Query(op)
+func ExecOpWithoutReply(socket *MongoSocket, op interface{}) error {
+ err := socket.Query(op)
if err != nil {
return err
}
@@ -4242,6 +4231,18 @@ func (s *Session) AcquireSocketPrivate(slaveOk bool) (*MongoSocket, error) {
return sock, nil
}
+func (s *Session) AcquireSocketDirect() (*MongoSocket, error) {
+ sock, err := s.cluster().AcquireSocket(Strong, true, s.syncTimeout, s.sockTimeout, s.queryConfig.op.ServerTags, s.poolLimit)
+ if err != nil {
+ return nil, err
+ }
+ if err = s.socketLogin(sock); err != nil {
+ sock.Release()
+ return nil, err
+ }
+ return sock, nil
+}
+
// setSocket binds socket to this section.
func (s *Session) setSocket(socket *MongoSocket) {
info := socket.Acquire()
diff --git a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init.go b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init.go
index 01ad6fefe97..314e5415c18 100644
--- a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init.go
+++ b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init.go
@@ -94,16 +94,17 @@ package openssl
#include <openssl/engine.h>
extern int Goopenssl_init_locks();
+extern unsigned long Goopenssl_thread_id_callback();
extern void Goopenssl_thread_locking_callback(int, int, const char*, int);
static int Goopenssl_init_threadsafety() {
- // Set up OPENSSL thread safety callbacks. We only set the locking
- // callback because the default id callback implementation is good
- // enough for us.
+ // Set up OPENSSL thread safety callbacks.
+ // TOOLS-1694 added setting of thread id callback for compatibility with openssl 0.9.8
int rc = Goopenssl_init_locks();
if (rc == 0) {
CRYPTO_set_locking_callback(Goopenssl_thread_locking_callback);
}
+ CRYPTO_set_id_callback(Goopenssl_thread_id_callback);
return rc;
}
diff --git a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_posix.go b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_posix.go
index 03ed0f01bd0..99558298e3a 100644
--- a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_posix.go
+++ b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_posix.go
@@ -52,6 +52,7 @@ int Goopenssl_init_locks() {
return rc;
}
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
void Goopenssl_thread_locking_callback(int mode, int n, const char *file,
int line) {
if (mode & CRYPTO_LOCK) {
@@ -60,5 +61,9 @@ void Goopenssl_thread_locking_callback(int mode, int n, const char *file,
pthread_mutex_unlock(&goopenssl_locks[n]);
}
}
+unsigned long Goopenssl_thread_id_callback() {
+ return (unsigned long) pthread_self();
+}
+#endif
*/
import "C"
diff --git a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_windows.go b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_windows.go
index 5eca9fa0eac..ec817926b7a 100644
--- a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_windows.go
+++ b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/openssl/init_windows.go
@@ -56,5 +56,10 @@ void Goopenssl_thread_locking_callback(int mode, int n, const char *file,
LeaveCriticalSection(&goopenssl_locks[n]);
}
}
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+unsigned long Goopenssl_thread_id_callback() {
+ return (unsigned long) GetCurrentThreadId();
+}
+#endif
*/
import "C"
diff --git a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_linux.go b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_linux.go
new file mode 100644
index 00000000000..160a3b3c1a9
--- /dev/null
+++ b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_linux.go
@@ -0,0 +1,33 @@
+// Copyright (C) 2014 Space Monkey, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package spacelog
+
+import (
+ "syscall"
+)
+
+// CaptureOutputToFd redirects the current process' stdout and stderr file
+// descriptors to the given file descriptor, using the dup3 syscall.
+func CaptureOutputToFd(fd int) error {
+ err := syscall.Dup3(fd, syscall.Stdout, 0)
+ if err != nil {
+ return err
+ }
+ err = syscall.Dup3(fd, syscall.Stderr, 0)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_other.go b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_other.go
index 5a62a2accaf..5fb22093798 100644
--- a/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_other.go
+++ b/src/mongo/gotools/vendor/src/github.com/spacemonkeygo/spacelog/capture_other.go
@@ -13,6 +13,7 @@
// limitations under the License.
// +build !windows
+// +build !linux
package spacelog
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson.go
index 7fb7f8cae48..c023b0638bb 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson.go
@@ -51,6 +51,8 @@ import (
"time"
)
+//go:generate go run bson_corpus_spec_test_generator.go
+
// --------------------------------------------------------------------------
// The public API.
@@ -279,7 +281,7 @@ var nullBytes = []byte("null")
func (id *ObjectId) UnmarshalJSON(data []byte) error {
if len(data) > 0 && (data[0] == '{' || data[0] == 'O') {
var v struct {
- Id json.RawMessage `json:"$oid"`
+ Id json.RawMessage `json:"$oid"`
Func struct {
Id json.RawMessage
} `json:"$oidFunc"`
@@ -561,6 +563,9 @@ func Unmarshal(in []byte, out interface{}) (err error) {
case reflect.Map:
d := newDecoder(in)
d.readDocTo(v)
+ if d.i < len(d.in) {
+ return errors.New("Document is corrupted")
+ }
case reflect.Struct:
return errors.New("Unmarshal can't deal with struct values. Use a pointer.")
default:
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson_test.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson_test.go
index 37451f9fdc2..11fdfae4185 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson_test.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/bson_test.go
@@ -29,19 +29,16 @@ package bson_test
import (
"encoding/binary"
- "encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"net/url"
"reflect"
- "strings"
"testing"
"time"
. "gopkg.in/check.v1"
"gopkg.in/mgo.v2/bson"
- "gopkg.in/yaml.v2"
)
func TestAll(t *testing.T) {
@@ -1581,65 +1578,6 @@ func (s *S) TestObjectIdJSONMarshaling(c *C) {
}
// --------------------------------------------------------------------------
-// Spec tests
-
-type specTest struct {
- Description string
- Documents []struct {
- Decoded map[string]interface{}
- Encoded string
- DecodeOnly bool `yaml:"decodeOnly"`
- Error interface{}
- }
-}
-
-func (s *S) TestSpecTests(c *C) {
- for _, data := range specTests {
- var test specTest
- err := yaml.Unmarshal([]byte(data), &test)
- c.Assert(err, IsNil)
-
- c.Logf("Running spec test set %q", test.Description)
-
- for _, doc := range test.Documents {
- if doc.Error != nil {
- continue
- }
- c.Logf("Ensuring %q decodes as %v", doc.Encoded, doc.Decoded)
- var decoded map[string]interface{}
- encoded, err := hex.DecodeString(doc.Encoded)
- c.Assert(err, IsNil)
- err = bson.Unmarshal(encoded, &decoded)
- c.Assert(err, IsNil)
- c.Assert(decoded, DeepEquals, doc.Decoded)
- }
-
- for _, doc := range test.Documents {
- if doc.DecodeOnly || doc.Error != nil {
- continue
- }
- c.Logf("Ensuring %v encodes as %q", doc.Decoded, doc.Encoded)
- encoded, err := bson.Marshal(doc.Decoded)
- c.Assert(err, IsNil)
- c.Assert(strings.ToUpper(hex.EncodeToString(encoded)), Equals, doc.Encoded)
- }
-
- for _, doc := range test.Documents {
- if doc.Error == nil {
- continue
- }
- c.Logf("Ensuring %q errors when decoded: %s", doc.Encoded, doc.Error)
- var decoded map[string]interface{}
- encoded, err := hex.DecodeString(doc.Encoded)
- c.Assert(err, IsNil)
- err = bson.Unmarshal(encoded, &decoded)
- c.Assert(err, NotNil)
- c.Logf("Failed with: %v", err)
- }
- }
-}
-
-// --------------------------------------------------------------------------
// ObjectId Text encoding.TextUnmarshaler.
var textIdTests = []struct {
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/decode.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/decode.go
index 7c2d8416afe..fd29da45071 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/decode.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/decode.go
@@ -225,61 +225,65 @@ func (d *decoder) readDocTo(out reflect.Value) {
panic("Unsupported document type for unmarshalling: " + out.Type().String())
}
- end := int(d.readInt32())
- end += d.i - 4
- if end <= d.i || end > len(d.in) || d.in[end-1] != '\x00' {
- corrupted()
- }
- for d.in[d.i] != '\x00' {
- kind := d.readByte()
- name := d.readCStr()
- if d.i >= end {
+ if outt == typeRaw {
+ d.skipDoc()
+ } else {
+ end := int(d.readInt32())
+ end += d.i - 4
+ if end <= d.i || end > len(d.in) || d.in[end-1] != '\x00' {
corrupted()
}
+ for d.in[d.i] != '\x00' {
+ kind := d.readByte()
+ name := d.readCStr()
+ if d.i >= end {
+ corrupted()
+ }
- switch outk {
- case reflect.Map:
- e := reflect.New(elemType).Elem()
- if d.readElemTo(e, kind) {
- k := reflect.ValueOf(name)
- if convertKey {
- k = k.Convert(keyType)
+ switch outk {
+ case reflect.Map:
+ e := reflect.New(elemType).Elem()
+ if d.readElemTo(e, kind) {
+ k := reflect.ValueOf(name)
+ if convertKey {
+ k = k.Convert(keyType)
+ }
+ out.SetMapIndex(k, e)
}
- out.SetMapIndex(k, e)
- }
- case reflect.Struct:
- if outt == typeRaw {
- d.dropElem(kind)
- } else {
- if info, ok := fieldsMap[name]; ok {
- if info.Inline == nil {
- d.readElemTo(out.Field(info.Num), kind)
+ case reflect.Struct:
+ if outt == typeRaw {
+ d.dropElem(kind)
+ } else {
+ if info, ok := fieldsMap[name]; ok {
+ if info.Inline == nil {
+ d.readElemTo(out.Field(info.Num), kind)
+ } else {
+ d.readElemTo(out.FieldByIndex(info.Inline), kind)
+ }
+ } else if inlineMap.IsValid() {
+ if inlineMap.IsNil() {
+ inlineMap.Set(reflect.MakeMap(inlineMap.Type()))
+ }
+ e := reflect.New(elemType).Elem()
+ if d.readElemTo(e, kind) {
+ inlineMap.SetMapIndex(reflect.ValueOf(name), e)
+ }
} else {
- d.readElemTo(out.FieldByIndex(info.Inline), kind)
- }
- } else if inlineMap.IsValid() {
- if inlineMap.IsNil() {
- inlineMap.Set(reflect.MakeMap(inlineMap.Type()))
- }
- e := reflect.New(elemType).Elem()
- if d.readElemTo(e, kind) {
- inlineMap.SetMapIndex(reflect.ValueOf(name), e)
+ d.dropElem(kind)
}
- } else {
- d.dropElem(kind)
}
+ case reflect.Slice:
}
- case reflect.Slice:
- }
- if d.i >= end {
+ if d.i >= end {
+ corrupted()
+ }
+ }
+ d.i++ // '\x00'
+ if d.i != end {
corrupted()
}
}
- d.i++ // '\x00'
- if d.i != end {
- corrupted()
- }
d.docType = docType
if outt == typeRaw {
@@ -431,7 +435,62 @@ func (d *decoder) readDocWith(f func(kind byte, name string)) {
var blackHole = settableValueOf(struct{}{})
func (d *decoder) dropElem(kind byte) {
- d.readElemTo(blackHole, kind)
+ switch kind {
+ case 0x01, 0x09, 0x11, 0x12: // double, utc datetime, timestamp, int64
+ d.i += 8
+ case 0x02, 0x0D, 0x0E: // string, javascript, symbol
+ l := int(d.readInt32())
+ if l <= 0 || d.i+l >= len(d.in) || d.in[d.i+l-1] != 0x00 {
+ corrupted()
+ }
+ d.i += l
+ case 0x03, 0x04: // doc, array
+ d.skipDoc()
+ case 0x05: // binary
+ l := int(d.readInt32())
+ k := d.readByte()
+ if k == 0x02 && l > 4 {
+ rl := int(d.readInt32())
+ if rl != l-4 {
+ corrupted()
+ }
+ }
+ d.i += l
+ case 0x06: // undefined
+ case 0x07: // objectID
+ d.i += 12
+ case 0x08:
+ k := d.readByte()
+ if k != 0x00 && k != 0x01 {
+ corrupted()
+ }
+ case 0x0A: // null
+ case 0x0B: // regex
+ d.readCStr()
+ d.readCStr()
+ case 0x0C: // dbpointer
+ d.dropElem(0x02)
+ d.i += 12
+ case 0x0F:
+ start := d.i
+ l := int(d.readInt32())
+ d.dropElem(0x02) // string
+ d.skipDoc()
+ if d.i != start+l {
+ corrupted()
+ }
+ case 0x10: // int32
+ d.i += 4
+ case 0x13: // decimal
+ d.i += 16
+ case 0xFF, 0x7F: //min key, max key
+ default:
+ d.readElemTo(blackHole, kind)
+ }
+
+ if d.i > len(d.in) {
+ corrupted()
+ }
}
// Attempt to decode an element from the document and put it into out.
@@ -461,11 +520,11 @@ func (d *decoder) readElemTo(out reflect.Value, kind byte) (good bool) {
case typeRawDocElem:
out.Set(d.readRawDocElems(outt))
default:
- d.readDocTo(blackHole)
+ d.skipDoc()
}
return true
}
- d.readDocTo(blackHole)
+ d.skipDoc()
return true
}
@@ -529,9 +588,13 @@ func (d *decoder) readElemTo(out reflect.Value, kind byte) (good bool) {
case 0x0E: // Symbol
in = Symbol(d.readStr())
case 0x0F: // JavaScript with scope
- d.i += 4 // Skip length
+ start := d.i
+ l := int(d.readInt32())
js := JavaScript{d.readStr(), make(M)}
d.readDocTo(reflect.ValueOf(js.Scope))
+ if d.i != start+l {
+ corrupted()
+ }
in = js
case 0x10: // Int32
in = int(d.readInt32())
@@ -748,6 +811,15 @@ func (d *decoder) readElemTo(out reflect.Value, kind byte) (good bool) {
// --------------------------------------------------------------------------
// Parsers of basic types.
+func (d *decoder) skipDoc() {
+ end := int(d.readInt32())
+ end += d.i - 4
+ if end <= d.i || end > len(d.in) || d.in[end-1] != '\x00' {
+ corrupted()
+ }
+ d.i = end
+}
+
func (d *decoder) readRegEx() RegEx {
re := RegEx{}
re.Pattern = d.readCStr()
@@ -759,11 +831,15 @@ func (d *decoder) readBinary() Binary {
l := d.readInt32()
b := Binary{}
b.Kind = d.readByte()
- b.Data = d.readBytes(l)
- if b.Kind == 0x02 && len(b.Data) >= 4 {
+ if b.Kind == 0x02 && l > 4 {
// Weird obsolete format with redundant length.
- b.Data = b.Data[4:]
+ rl := d.readInt32()
+ if rl != l-4 {
+ corrupted()
+ }
+ l = rl
}
+ b.Data = d.readBytes(l)
return b
}
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/encode.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/encode.go
index add39e865dd..25f1adc6303 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/encode.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/encode.go
@@ -33,6 +33,7 @@ import (
"math"
"net/url"
"reflect"
+ "sort"
"strconv"
"time"
)
@@ -419,7 +420,9 @@ func (e *encoder) addElem(name string, v reflect.Value, minSize bool) {
case RegEx:
e.addElemName(0x0B, name)
e.addCStr(s.Pattern)
- e.addCStr(s.Options)
+ options := runes(s.Options)
+ sort.Sort(options)
+ e.addCStr(string(options))
case JavaScript:
if s.Scope == nil {
@@ -455,6 +458,14 @@ func (e *encoder) addElem(name string, v reflect.Value, minSize bool) {
}
}
+// -------------
+// Helper method for sorting regex options
+type runes []rune
+
+func (a runes) Len() int { return len(a) }
+func (a runes) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a runes) Less(i, j int) bool { return a[i] < a[j] }
+
// --------------------------------------------------------------------------
// Marshaling of base types.
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata/update.sh b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata/update.sh
index 1efd3d3b66d..7b1141c105e 100755
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata/update.sh
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata/update.sh
@@ -1,27 +1,9 @@
-#!/bin/sh
+#/bin/sh
set -e
-if [ ! -d specifications ]; then
- git clone -b bson git@github.com:jyemin/specifications
-fi
+rm -rf specifications
-TESTFILE="../specdata_test.go"
+git clone git@github.com:mongodb/specifications
-cat <<END > $TESTFILE
-package bson_test
-
-var specTests = []string{
-END
-
-for file in specifications/source/bson/tests/*.yml; do
- (
- echo '`'
- cat $file
- echo -n '`,'
- ) >> $TESTFILE
-done
-
-echo '}' >> $TESTFILE
-
-gofmt -w $TESTFILE
+go generate ../ \ No newline at end of file
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata_test.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata_test.go
deleted file mode 100644
index 513f9b209c7..00000000000
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/bson/specdata_test.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package bson_test
-
-var specTests = []string{
- `
----
-description: "Array type"
-documents:
- -
- decoded:
- a : []
- encoded: 0D000000046100050000000000
- -
- decoded:
- a: [10]
- encoded: 140000000461000C0000001030000A0000000000
- -
- # Decode an array that uses an empty string as the key
- decodeOnly : true
- decoded:
- a: [10]
- encoded: 130000000461000B00000010000A0000000000
- -
- # Decode an array that uses a non-numeric string as the key
- decodeOnly : true
- decoded:
- a: [10]
- encoded: 150000000461000D000000106162000A0000000000
-
-
-`, `
----
-description: "Boolean type"
-documents:
- -
- encoded: "090000000862000100"
- decoded: { "b" : true }
- -
- encoded: "090000000862000000"
- decoded: { "b" : false }
-
-
- `, `
----
-description: "Corrupted BSON"
-documents:
- -
- encoded: "09000000016600"
- error: "truncated double"
- -
- encoded: "09000000026600"
- error: "truncated string"
- -
- encoded: "09000000036600"
- error: "truncated document"
- -
- encoded: "09000000046600"
- error: "truncated array"
- -
- encoded: "09000000056600"
- error: "truncated binary"
- -
- encoded: "09000000076600"
- error: "truncated objectid"
- -
- encoded: "09000000086600"
- error: "truncated boolean"
- -
- encoded: "09000000096600"
- error: "truncated date"
- -
- encoded: "090000000b6600"
- error: "truncated regex"
- -
- encoded: "090000000c6600"
- error: "truncated db pointer"
- -
- encoded: "0C0000000d6600"
- error: "truncated javascript"
- -
- encoded: "0C0000000e6600"
- error: "truncated symbol"
- -
- encoded: "0C0000000f6600"
- error: "truncated javascript with scope"
- -
- encoded: "0C000000106600"
- error: "truncated int32"
- -
- encoded: "0C000000116600"
- error: "truncated timestamp"
- -
- encoded: "0C000000126600"
- error: "truncated int64"
- -
- encoded: "0400000000"
- error: basic
- -
- encoded: "0500000001"
- error: basic
- -
- encoded: "05000000"
- error: basic
- -
- encoded: "0700000002610078563412"
- error: basic
- -
- encoded: "090000001061000500"
- error: basic
- -
- encoded: "00000000000000000000"
- error: basic
- -
- encoded: "1300000002666f6f00040000006261720000"
- error: "basic"
- -
- encoded: "1800000003666f6f000f0000001062617200ffffff7f0000"
- error: basic
- -
- encoded: "1500000003666f6f000c0000000862617200010000"
- error: basic
- -
- encoded: "1c00000003666f6f001200000002626172000500000062617a000000"
- error: basic
- -
- encoded: "1000000002610004000000616263ff00"
- error: string is not null-terminated
- -
- encoded: "0c0000000200000000000000"
- error: bad_string_length
- -
- encoded: "120000000200ffffffff666f6f6261720000"
- error: bad_string_length
- -
- encoded: "0c0000000e00000000000000"
- error: bad_string_length
- -
- encoded: "120000000e00ffffffff666f6f6261720000"
- error: bad_string_length
- -
- encoded: "180000000c00fa5bd841d6585d9900"
- error: ""
- -
- encoded: "1e0000000c00ffffffff666f6f626172005259b56afa5bd841d6585d9900"
- error: bad_string_length
- -
- encoded: "0c0000000d00000000000000"
- error: bad_string_length
- -
- encoded: "0c0000000d00ffffffff0000"
- error: bad_string_length
- -
- encoded: "1c0000000f001500000000000000000c000000020001000000000000"
- error: bad_string_length
- -
- encoded: "1c0000000f0015000000ffffffff000c000000020001000000000000"
- error: bad_string_length
- -
- encoded: "1c0000000f001500000001000000000c000000020000000000000000"
- error: bad_string_length
- -
- encoded: "1c0000000f001500000001000000000c0000000200ffffffff000000"
- error: bad_string_length
- -
- encoded: "0E00000008616263646566676869707172737475"
- error: "Run-on CString"
- -
- encoded: "0100000000"
- error: "An object size that's too small to even include the object size, but is correctly encoded, along with a correct EOO (and no data)"
- -
- encoded: "1a0000000e74657374000c00000068656c6c6f20776f726c6400000500000000"
- error: "One object, but with object size listed smaller than it is in the data"
- -
- encoded: "05000000"
- error: "One object, missing the EOO at the end"
- -
- encoded: "0500000001"
- error: "One object, sized correctly, with a spot for an EOO, but the EOO is 0x01"
- -
- encoded: "05000000ff"
- error: "One object, sized correctly, with a spot for an EOO, but the EOO is 0xff"
- -
- encoded: "0500000070"
- error: "One object, sized correctly, with a spot for an EOO, but the EOO is 0x70"
- -
- encoded: "07000000000000"
- error: "Invalid BSON type low range"
- -
- encoded: "07000000800000"
- error: "Invalid BSON type high range"
- -
- encoded: "090000000862000200"
- error: "Invalid boolean value of 2"
- -
- encoded: "09000000086200ff00"
- error: "Invalid boolean value of -1"
- `, `
----
-description: "Int32 type"
-documents:
- -
- decoded:
- i: -2147483648
- encoded: 0C0000001069000000008000
- -
- decoded:
- i: 2147483647
- encoded: 0C000000106900FFFFFF7F00
- -
- decoded:
- i: -1
- encoded: 0C000000106900FFFFFFFF00
- -
- decoded:
- i: 0
- encoded: 0C0000001069000000000000
- -
- decoded:
- i: 1
- encoded: 0C0000001069000100000000
-
-`, `
----
-description: "String type"
-documents:
- -
- decoded:
- s : ""
- encoded: 0D000000027300010000000000
- -
- decoded:
- s: "a"
- encoded: 0E00000002730002000000610000
- -
- decoded:
- s: "This is a string"
- encoded: 1D0000000273001100000054686973206973206120737472696E670000
- -
- decoded:
- s: "κόσμε"
- encoded: 180000000273000C000000CEBAE1BDB9CF83CEBCCEB50000
-`}
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/decode.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/decode.go
index ce7c7d2493d..2171d91a711 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/decode.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/decode.go
@@ -773,7 +773,7 @@ func (d *decodeState) isNull(off int) bool {
// name consumes a const or function from d.data[d.off-1:], decoding into the value v.
// the first byte of the function name has been read already.
func (d *decodeState) name(v reflect.Value) {
- if d.isNull(d.off-1) {
+ if d.isNull(d.off - 1) {
d.literal(v)
return
}
@@ -1036,7 +1036,7 @@ func (d *decodeState) keyed() (interface{}, bool) {
break
}
- name := d.data[d.off-1+start : d.off-1+end]
+ name := bytes.Trim(d.data[d.off-1+start:d.off-1+end], " \n\t")
var key []byte
var ok bool
@@ -1076,9 +1076,9 @@ func (d *decodeState) storeKeyed(v reflect.Value) bool {
}
var (
- trueBytes = []byte("true")
+ trueBytes = []byte("true")
falseBytes = []byte("false")
- nullBytes = []byte("null")
+ nullBytes = []byte("null")
)
func (d *decodeState) storeValue(v reflect.Value, from interface{}) {
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/extension_test.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/extension_test.go
index 8c228189724..c5db5cf559f 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/extension_test.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/extension_test.go
@@ -131,6 +131,9 @@ var extDecodeTests = []extDecodeTest{
{in: `Const1`, ptr: new(interface{}), out: const1},
{in: `{"c": Const1}`, ptr: new(struct{ C *const1Type }), out: struct{ C *const1Type }{const1}},
+ // Space after quoted key
+ {in: `{"c" : Const1}`, ptr: new(struct{ C *const1Type }), out: struct{ C *const1Type }{const1}},
+
// Keyed documents
{in: `{"v": {"$key1": 1}}`, ptr: new(interface{}), out: map[string]interface{}{"v": keyed(`{"$key1": 1}`)}},
{in: `{"k": {"$key1": 1}}`, ptr: new(keyedType), out: keyedType{K: keyed(`{"$key1": 1}`)}},
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/stream_test.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/stream_test.go
index 0abdf7b5654..4ebeaba961c 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/stream_test.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/internal/json/stream_test.go
@@ -131,7 +131,7 @@ func TestDecoder(t *testing.T) {
for _, c := range nlines(streamEncoded, i) {
// That's stupid isn't it!? nulltrue!?!? :/
//if c != '\n' {
- buf.WriteRune(c)
+ buf.WriteRune(c)
//}
}
out := make([]interface{}, i)
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session.go
index 12ca8f2ac37..108cdae8848 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session.go
@@ -279,44 +279,81 @@ func ParseURL(url string) (*DialInfo, error) {
source := ""
setName := ""
poolLimit := 0
- for k, v := range uinfo.options {
- switch k {
+ readPreferenceMode := Primary
+ var readPreferenceTagSets []bson.D
+ for _, opt := range uinfo.options {
+ switch opt.key {
case "authSource":
- source = v
+ source = opt.value
case "authMechanism":
- mechanism = v
+ mechanism = opt.value
case "gssapiServiceName":
- service = v
+ service = opt.value
case "replicaSet":
- setName = v
+ setName = opt.value
case "maxPoolSize":
- poolLimit, err = strconv.Atoi(v)
+ poolLimit, err = strconv.Atoi(opt.value)
if err != nil {
- return nil, errors.New("bad value for maxPoolSize: " + v)
+ return nil, errors.New("bad value for maxPoolSize: " + opt.value)
}
+ case "readPreference":
+ switch opt.value {
+ case "nearest":
+ readPreferenceMode = Nearest
+ case "primary":
+ readPreferenceMode = Primary
+ case "primaryPreferred":
+ readPreferenceMode = PrimaryPreferred
+ case "secondary":
+ readPreferenceMode = Secondary
+ case "secondaryPreferred":
+ readPreferenceMode = SecondaryPreferred
+ default:
+ return nil, errors.New("bad value for readPreference: " + opt.value)
+ }
+ case "readPreferenceTags":
+ tags := strings.Split(opt.value, ",")
+ var doc bson.D
+ for _, tag := range tags {
+ kvp := strings.Split(tag, ":")
+ if len(kvp) != 2 {
+ return nil, errors.New("bad value for readPreferenceTags: " + opt.value)
+ }
+ doc = append(doc, bson.DocElem{Name: strings.TrimSpace(kvp[0]), Value: strings.TrimSpace(kvp[1])})
+ }
+ readPreferenceTagSets = append(readPreferenceTagSets, doc)
case "connect":
- if v == "direct" {
+ if opt.value == "direct" {
direct = true
break
}
- if v == "replicaSet" {
+ if opt.value == "replicaSet" {
break
}
fallthrough
default:
- return nil, errors.New("unsupported connection URL option: " + k + "=" + v)
+ return nil, errors.New("unsupported connection URL option: " + opt.key + "=" + opt.value)
}
}
+
+ if readPreferenceMode == Primary && len(readPreferenceTagSets) > 0 {
+ return nil, errors.New("readPreferenceTagSet may not be specified when readPreference is primary")
+ }
+
info := DialInfo{
- Addrs: uinfo.addrs,
- Direct: direct,
- Database: uinfo.db,
- Username: uinfo.user,
- Password: uinfo.pass,
- Mechanism: mechanism,
- Service: service,
- Source: source,
- PoolLimit: poolLimit,
+ Addrs: uinfo.addrs,
+ Direct: direct,
+ Database: uinfo.db,
+ Username: uinfo.user,
+ Password: uinfo.pass,
+ Mechanism: mechanism,
+ Service: service,
+ Source: source,
+ PoolLimit: poolLimit,
+ ReadPreference: &ReadPreference{
+ Mode: readPreferenceMode,
+ TagSets: readPreferenceTagSets,
+ },
ReplicaSetName: setName,
}
return &info, nil
@@ -384,6 +421,10 @@ type DialInfo struct {
// See Session.SetPoolLimit for details.
PoolLimit int
+ // ReadPreference defines the manner in which servers are chosen. See
+ // Session.SetMode and Session.SelectServers.
+ ReadPreference *ReadPreference
+
// DialServer optionally specifies the dial function for establishing
// connections with the MongoDB servers.
DialServer func(addr *ServerAddr) (net.Conn, error)
@@ -392,6 +433,15 @@ type DialInfo struct {
Dial func(addr net.Addr) (net.Conn, error)
}
+// ReadPreference defines the manner in which servers are chosen.
+type ReadPreference struct {
+ // Mode determines the consistency of results. See Session.SetMode.
+ Mode Mode
+
+ // TagSets indicates which servers are allowed to be used. See Session.SelectServers.
+ TagSets []bson.D
+}
+
// mgo.v3: Drop DialInfo.Dial.
// ServerAddr represents the address for establishing a connection to an
@@ -464,7 +514,14 @@ func DialWithInfo(info *DialInfo) (*Session, error) {
session.Close()
return nil, err
}
- session.SetMode(Strong, true)
+
+ if info.ReadPreference != nil {
+ session.SelectServers(info.ReadPreference.TagSets...)
+ session.SetMode(info.ReadPreference.Mode, true)
+ } else {
+ session.SetMode(Strong, true)
+ }
+
return session, nil
}
@@ -477,21 +534,26 @@ type urlInfo struct {
user string
pass string
db string
- options map[string]string
+ options []urlInfoOption
+}
+
+type urlInfoOption struct {
+ key string
+ value string
}
func extractURL(s string) (*urlInfo, error) {
if strings.HasPrefix(s, "mongodb://") {
s = s[10:]
}
- info := &urlInfo{options: make(map[string]string)}
+ info := &urlInfo{}
if c := strings.Index(s, "?"); c != -1 {
for _, pair := range strings.FieldsFunc(s[c+1:], isOptSep) {
l := strings.SplitN(pair, "=", 2)
if len(l) != 2 || l[0] == "" || l[1] == "" {
return nil, errors.New("connection option must be key=value: " + pair)
}
- info.options[l[0]] = l[1]
+ info.options = append(info.options, urlInfoOption{key: l[0], value: l[1]})
}
s = s[:c]
}
@@ -569,6 +631,17 @@ func (s *Session) LiveServers() (addrs []string) {
return addrs
}
+// ReadableServer returns a server address which is suitable for reading
+// according to the current session.
+func (s *Session) ReadableServer() (string, error) {
+ socket, err := s.acquireSocket(true)
+ if err != nil {
+ return "", err
+ }
+ defer socket.Release()
+ return socket.server.Addr, nil
+}
+
// DB returns a value representing the named database. If name
// is empty, the database name provided in the dialed URL is
// used instead. If that is also empty, "test" is used as a
@@ -1096,6 +1169,14 @@ type Collation struct {
// distinguished at strength > 3). Defaults to "non-ignorable".
Alternate string `bson:"alternate,omitempty"`
+ // MaxVariable defines which characters are affected when the value for Alternate is
+ // "shifted". It may be set to "punct" to affect punctuation or spaces, or "space" to
+ // affect only spaces.
+ MaxVariable string `bson:"maxVariable,omitempty"`
+
+ // Normalization defines whether text is normalized into Unicode NFD.
+ Normalization bool `bson:"normalization,omitempty"`
+
// Backwards defines whether to have secondary differences considered in reverse order,
// as done in the French language.
Backwards bool `bson:"backwards,omitempty"`
@@ -2299,6 +2380,11 @@ func (c *Collection) NewIter(session *Session, firstBatch []bson.Raw, cursorId i
timeout: -1,
err: err,
}
+
+ if socket.ServerInfo().MaxWireVersion >= 4 && c.FullName != "admin.$cmd" {
+ iter.findCmd = true
+ }
+
iter.gotReply.L = &iter.m
for _, doc := range firstBatch {
iter.docData.Push(doc.Data)
@@ -2858,6 +2944,14 @@ func (q *Query) Sort(fields ...string) *Query {
return q
}
+func (q *Query) Collation(collation *Collation) *Query {
+ q.m.Lock()
+ q.op.options.Collation = collation
+ q.op.hasOptions = true
+ q.m.Unlock()
+ return q
+}
+
// Explain returns a number of details about how the MongoDB server would
// execute the requested query, such as the number of objects examined,
// the number of times the read lock was yielded to allow writes to go in,
@@ -3161,6 +3255,7 @@ func prepareFindOp(socket *mongoSocket, op *queryOp, limit int32) bool {
Comment: op.options.Comment,
Snapshot: op.options.Snapshot,
OplogReplay: op.flags&flagLogReplay != 0,
+ Collation: op.options.Collation,
}
if op.limit < 0 {
find.BatchSize = -op.limit
@@ -3222,6 +3317,7 @@ type findCmd struct {
OplogReplay bool `bson:"oplogReplay,omitempty"`
NoCursorTimeout bool `bson:"noCursorTimeout,omitempty"`
AllowPartialResults bool `bson:"allowPartialResults,omitempty"`
+ Collation *Collation `bson:"collation,omitempty"`
}
// getMoreCmd holds the command used for requesting more query results on MongoDB 3.2+.
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session_test.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session_test.go
index a89279d38b1..314ace5cd5c 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session_test.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/session_test.go
@@ -132,6 +132,93 @@ func (s *S) TestURLParsing(c *C) {
}
}
+func (s *S) TestURLReadPreference(c *C) {
+ type test struct {
+ url string
+ mode mgo.Mode
+ }
+
+ tests := []test{
+ {"localhost:40001?readPreference=primary", mgo.Primary},
+ {"localhost:40001?readPreference=primaryPreferred", mgo.PrimaryPreferred},
+ {"localhost:40001?readPreference=secondary", mgo.Secondary},
+ {"localhost:40001?readPreference=secondaryPreferred", mgo.SecondaryPreferred},
+ {"localhost:40001?readPreference=nearest", mgo.Nearest},
+ }
+
+ for _, test := range tests {
+ info, err := mgo.ParseURL(test.url)
+ c.Assert(err, IsNil)
+ c.Assert(info.ReadPreference, NotNil)
+ c.Assert(info.ReadPreference.Mode, Equals, test.mode)
+ }
+}
+
+func (s *S) TestURLInvalidReadPreference(c *C) {
+ urls := []string{
+ "localhost:40001?readPreference=foo",
+ "localhost:40001?readPreference=primarypreferred",
+ }
+ for _, url := range urls {
+ _, err := mgo.ParseURL(url)
+ c.Assert(err, NotNil)
+ }
+}
+
+func (s *S) TestURLReadPreferenceTags(c *C) {
+ type test struct {
+ url string
+ tagSets []bson.D
+ }
+
+ tests := []test{
+ {"localhost:40001?readPreference=secondary&readPreferenceTags=dc:ny,rack:1", []bson.D{{{"dc", "ny"}, {"rack", "1"}}}},
+ {"localhost:40001?readPreference=secondary&readPreferenceTags= dc : ny , rack : 1 ", []bson.D{{{"dc", "ny"}, {"rack", "1"}}}},
+ {"localhost:40001?readPreference=secondary&readPreferenceTags=dc:ny", []bson.D{{{"dc", "ny"}}}},
+ {"localhost:40001?readPreference=secondary&readPreferenceTags=rack:1&readPreferenceTags=dc:ny", []bson.D{{{"rack", "1"}}, {{"dc", "ny"}}}},
+ }
+
+ for _, test := range tests {
+ info, err := mgo.ParseURL(test.url)
+ c.Assert(err, IsNil)
+ c.Assert(info.ReadPreference, NotNil)
+ c.Assert(info.ReadPreference.TagSets, DeepEquals, test.tagSets)
+ }
+}
+
+func (s *S) TestURLInvalidReadPreferenceTags(c *C) {
+ urls := []string{
+ "localhost:40001?readPreference=secondary&readPreferenceTags=dc",
+ "localhost:40001?readPreference=secondary&readPreferenceTags=dc:ny,rack",
+ "localhost:40001?readPreference=secondary&readPreferenceTags=dc,rack",
+ "localhost:40001?readPreference=primary&readPreferenceTags=dc:ny",
+ }
+ for _, url := range urls {
+ _, err := mgo.ParseURL(url)
+ c.Assert(err, NotNil)
+ }
+}
+
+func (s *S) TestReadableServer(c *C) {
+ session, err := mgo.Dial("localhost:40011,localhost:40012,localhost:40013")
+ c.Assert(err, IsNil)
+ defer session.Close()
+
+ session.SetMode(mgo.Primary, true)
+ primary_address, err := session.ReadableServer()
+ c.Assert(err, IsNil)
+ c.Assert(primary_address, Equals, "localhost:40011")
+
+ session.SetMode(mgo.Secondary, true)
+ secondary_address, err := session.ReadableServer()
+ c.Assert(err, IsNil)
+
+ valid_addresses := []string{"localhost:40012", "localhost:40013"}
+ if secondary_address != valid_addresses[0] && secondary_address != valid_addresses[1] {
+ c.Fatalf("secondary_address should be in %v, not: %v", valid_addresses, secondary_address)
+ }
+}
+
func (s *S) TestInsertFindOne(c *C) {
session, err := mgo.Dial("localhost:40001")
c.Assert(err, IsNil)
@@ -4159,11 +4246,11 @@ func (s *S) TestBypassValidation(c *C) {
func (s *S) TestVersionAtLeast(c *C) {
tests := [][][]int{
- {{3,2,1}, {3,2,0}},
- {{3,2,1}, {3,2}},
- {{3,2,1}, {2,5,5,5}},
- {{3,2,1}, {2,5,5}},
- {{3,2,1}, {2,5}},
+ {{3, 2, 1}, {3, 2, 0}},
+ {{3, 2, 1}, {3, 2}},
+ {{3, 2, 1}, {2, 5, 5, 5}},
+ {{3, 2, 1}, {2, 5, 5}},
+ {{3, 2, 1}, {2, 5}},
}
for _, pair := range tests {
bi := mgo.BuildInfo{VersionArray: pair[0]}
@@ -4180,6 +4267,48 @@ func (s *S) TestVersionAtLeast(c *C) {
}
}
+func (s *S) TestCollationQueries(c *C) {
+ if !s.versionAtLeast(3, 3, 12) {
+ c.Skip("collations being released with 3.4")
+ }
+ session, err := mgo.Dial("localhost:40001")
+ c.Assert(err, IsNil)
+ defer session.Close()
+
+ docsToInsert := []bson.M{
+ {"text_number": "010"},
+ {"text_number": "2"},
+ {"text_number": "10"},
+ }
+
+ coll := session.DB("mydb").C("mycoll")
+ for _, doc := range docsToInsert {
+ err = coll.Insert(doc)
+ c.Assert(err, IsNil)
+ }
+
+ collation := &mgo.Collation{
+ Locale: "en",
+ NumericOrdering: true,
+ }
+
+ err = coll.EnsureIndex(mgo.Index{
+ Key: []string{"text_number"},
+ Collation: collation,
+ })
+ c.Assert(err, IsNil)
+
+ iter := coll.Find(nil).Sort("text_number").Collation(collation).Iter()
+ defer iter.Close()
+ for _, expectedRes := range []string{"2", "010", "10"} {
+ res := make(bson.M)
+ found := iter.Next(&res)
+ c.Assert(iter.Err(), IsNil)
+ c.Assert(found, Equals, true)
+ c.Assert(res["text_number"], Equals, expectedRes)
+ }
+}
+
// --------------------------------------------------------------------------
// Some benchmarks that require a running database.
diff --git a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/socket.go b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/socket.go
index 8891dd5d734..f4941269217 100644
--- a/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/socket.go
+++ b/src/mongo/gotools/vendor/src/gopkg.in/mgo.v2/socket.go
@@ -91,6 +91,7 @@ type queryWrapper struct {
MaxScan int "$maxScan,omitempty"
MaxTimeMS int "$maxTimeMS,omitempty"
Comment string "$comment,omitempty"
+ Collation *Collation "$collation,omitempty"
}
func (op *queryOp) finalQuery(socket *mongoSocket) interface{} {