mongo_delta 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ Gemfile.lock
2
+ .bundle
3
+ vendor/cache
4
+ vendor/ruby
5
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013, Secret Sauce Partners, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ Mongo Delta
2
+ ===========
3
+
4
+ ### Coordinated transfer between MongoDB clusters
5
+
6
+ Mongo Delta is a command line tool that tails a MongoDB replica set's
7
+ oplog (using [mongoriver](https://github.com/stripe/mongoriver)) and
8
+ based on a configured set of outlets transfers documents to other
9
+ MongoDB instances.
10
+
11
+ Installation
12
+ ------------
13
+
14
+ Install from Rubygems as:
15
+
16
+ $ gem install mongo_delta
17
+
18
+ Or build from source by:
19
+
20
+ $ gem build mongo_delta.gemspec
21
+
22
+ And then install the built gem.
23
+
24
+ Configuration
25
+ -------------
26
+
27
+ Mongo Delta requires a configuration where you set up your source,
28
+ various targets and outlets. This can be stored in a YAML file or in the
29
+ source database.
30
+
31
+ Here's an example:
32
+
33
+ ``` yaml
34
+ db: mongo_delta
35
+ service: mongo_delta
36
+
37
+ source: mongodb://mongorsa1:27017,mongorsa2:27017
38
+
39
+ targets:
40
+ archive: mongodb://mongoarch:27017
41
+
42
+ outlets:
43
+ - outlet: Replicator
44
+ target: archive
45
+ db: db_name
46
+ collection: events
47
+ ```
48
+
49
+ The `db` and `service` options are optional and do the same as their
50
+ command line counterparts. The default for both is `'mongo_delta'`. This
51
+ tells Mongo Delta where to persist the optime which tracks the point of
52
+ time upto which the oplog has been processed. The `service` option
53
+ makes it possible to run multiple Mongo Delta processes using the same
54
+ source.
55
+
56
+ The `source` is where Mongo Delta is going to tail the oplog.
57
+ Under `targets` several target connections can be listed.
58
+ Use [MongoDB URIs](http://api.mongodb.org/ruby/current/#Environment_variable_MONGODB_URI) for both options.
59
+
60
+ Finally, list outlets which will handle the incoming data and send them
61
+ out another way. Configure each outlet with the following options:
62
+
63
+ * `outlet`: name of one of the outlet implementations (see below)
64
+ * `target`: name of one of the targets
65
+ * `db` and `collection`: specify the namespace for which the outlet applies
66
+ * `target_db` and `target_collection`: optional, send data at target to
67
+ a different db and collection
68
+ * some outlets can have further options
69
+
70
+ ### Storing configuration in the source database
71
+
72
+ You can store this configuration in the source database. Use the
73
+ `--source` command line option and Mongo Delta will assume that the
74
+ configuration is located in the `config` collection of the `mongo_delta`
75
+ database with `_id: 'mongo_delta'`. The database and the service ID can
76
+ be overridden with the `--db` and `--service` options respectively.
77
+
78
+ Example:
79
+
80
+ ```
81
+ $ mongo mongo_delta
82
+ rs0:PRIMARY> db.config.save({
83
+ ... _id: 'mongo_delta',
84
+ ... outlets: [{
85
+ ... outlet: 'Replicator',
86
+ ... target: 'live',
87
+ ... db: 'sourcedb',
88
+ ... collection: 'events',
89
+ ... target_db: 'archive'
90
+ ... }],
91
+ ... targets: {live: 'mongodb://localhost:27017'}
92
+ ... })
93
+ $ mongo_delta --source mongodb://localhost:27017
94
+ 2013-06-10 21:24:29 - INFO: Registering Replicator outlet for cartman.events
95
+ 2013-06-10 21:24:29 - INFO: Starting stream
96
+ ```
97
+
98
+ Usage
99
+ -----
100
+
101
+ mongo_delta --config path/to/config.yml [options]
102
+
103
+ or if the configuration is stored in the source database:
104
+
105
+ mongo_delta --source mongodb://mongorsa1:27017,mongorsa2:27017 [options]
106
+
107
+ Run `mongo_delta --help` for more options.
108
+
109
+ Outlets
110
+ -------
111
+
112
+ ### Replicator
113
+
114
+ This outlet simply repeats `insert`, `remove` and `update` operations on
115
+ the configured target. You can use this to keep a remote collection
116
+ in sync with your main MongoDB cluster. Keep in mind that the
117
+ replication is one-way.
118
+
119
+ Sharded clusters
120
+ ----------------
121
+
122
+ Mongo Delta does not have special support for sharded Mongo clusters at
123
+ this time. It should be possible to run a separate `mongo_delta`
124
+ instance against each of the individual backend shard replica sets,
125
+ but otherwise with the same configuration.
126
+
127
+ Development
128
+ -----------
129
+
130
+ Patches and contributions are welcome! Please fork the project and
131
+ open a pull request on [github](https://github.com/sspinc/mongo_delta),
132
+ or just report issues.
133
+
134
+ Mongo Delta assumes the source MongoDB to be a replica set member. You
135
+ can create a standalone replica set member on your development machine
136
+ by running `mongod` with the `--replSet rs0` option, and then running
137
+ the following command in the mongo shell:
138
+
139
+ ``` javascript
140
+ rs.initiate({_id: 'rs0', members: [{ _id: 0, host: '127.0.0.1:27017'}]})
141
+ ```
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/setup'
4
+ require 'bundler/gem_tasks'
data/bin/mongo_delta ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mongo_delta/cli'
4
+
5
+ MongoDelta::CLI.start
@@ -0,0 +1,12 @@
1
+ source: mongodb://localhost:27017
2
+
3
+ targets:
4
+ archive: mongodb://localhost:27017
5
+
6
+ outlets:
7
+ - outlet: Replicator
8
+ target: archive
9
+ db: dbname
10
+ collection: events
11
+ target_db: archive # optional, defaults to 'db'
12
+ target_collection: archived_events # optional, defaults to 'collection'
@@ -0,0 +1,27 @@
1
+ module MongoDelta
2
+ class Agent < Mongoriver::AbstractOutlet
3
+
4
+ include MongoDelta::Logging
5
+
6
+ attr_reader :outlets
7
+
8
+ def initialize
9
+ @outlets = []
10
+ end
11
+
12
+ %w(insert remove update).each do |method|
13
+
14
+ define_method(method) do |db, collection, *args|
15
+ logger.debug "#{method} for #{db}.#{collection}: #{args.map(&:inspect).join(' ')}"
16
+
17
+ outlets.each do |outlet|
18
+ if outlet.handles?(db, collection, method)
19
+ outlet.send(method, *args)
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,90 @@
1
+ require 'optparse'
2
+
3
+ require 'mongo_delta'
4
+
5
+ module MongoDelta
6
+ module CLI
7
+ class << self
8
+
9
+ include MongoDelta::Logging
10
+
11
+ attr_accessor :config_path, :source_uri, :optime, :db, :service
12
+
13
+ def start
14
+ parse_options
15
+ validate_options!
16
+
17
+ config =
18
+ if config_path
19
+ Configuration.load_from_file(config_path)
20
+ else
21
+ Configuration.load_from_db(source_uri, db, service)
22
+ end
23
+ config.db = db
24
+ config.service = service
25
+
26
+ stream = Stream.new(config)
27
+ stream.run(optime)
28
+ rescue Mongo::ConnectionFailure, Configuration::Error
29
+ logger.fatal($!.message)
30
+ exit(1)
31
+ end
32
+
33
+ private
34
+
35
+ def parse_options
36
+ logger.level = Logger::INFO
37
+
38
+ optparse = OptionParser.new do |opts|
39
+ opts.banner = "Usage: #{$0} [options]"
40
+
41
+ opts.on('-v', '--verbose', 'More verbose output') do
42
+ logger.level = Logger::DEBUG
43
+ end
44
+
45
+ opts.on('-q', '--quiet', 'Less verbose output') do
46
+ logger.level = Logger::WARN
47
+ end
48
+
49
+ opts.on('--help', 'Display this message') do
50
+ puts opts
51
+ exit(1)
52
+ end
53
+
54
+ opts.on('-c FILE', '--config', 'Configuration file') do |path|
55
+ self.config_path = path
56
+ end
57
+
58
+ opts.on('-s MONGODB_URI', '--source', 'MongoDB URI for source connection') do |uri|
59
+ self.source_uri = uri
60
+ end
61
+
62
+ opts.on('-d DB', '--db', "DB for storing optime (default: #{Configuration::DEFAULT_DB})") do |db|
63
+ self.db = db
64
+ end
65
+
66
+ opts.on('-n NAME', '--service', "Service name (default: #{Configuration::DEFAULT_SERVICE})") do |service|
67
+ self.service = service
68
+ end
69
+
70
+ opts.on('-o OPTIME', '--optime', 'Starting optime') do |optime|
71
+ self.optime = Integer(optime)
72
+ end
73
+ end
74
+ optparse.parse!
75
+ end
76
+
77
+ def validate_options!
78
+ unless config_path or source_uri
79
+ logger.fatal "Missing configuration. Use --config or --source."
80
+ exit 1
81
+ end
82
+ if config_path and source_uri
83
+ logger.fatal "Options --config and --source are mutually exclusive."
84
+ exit 1
85
+ end
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,81 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'mongo'
4
+
5
+ module MongoDelta
6
+ class Configuration
7
+
8
+ class Error < RuntimeError; end
9
+
10
+ DEFAULT_DB='mongo_delta'
11
+ DEFAULT_SERVICE='mongo_delta'
12
+
13
+ def self.load_from_file(path)
14
+ options = YAML.load(ERB.new(File.read(path)).result)
15
+ new(options)
16
+ end
17
+
18
+ def self.load_from_db(mongodb_uri, db=nil, service=nil)
19
+ db ||= DEFAULT_DB
20
+ service ||= DEFAULT_SERVICE
21
+
22
+ mongo = connect_to_source(mongodb_uri)
23
+ collection = mongo.db(db).collection('config')
24
+
25
+ unless options = collection.find_one(:_id => service)
26
+ raise Error, "There was no config in the database at #{mongodb_uri}/#{db} with id '#{service}'"
27
+ end
28
+
29
+ new(options.merge({'source' => mongo, 'db' => db, 'service' => service}))
30
+ end
31
+
32
+ def self.connect_to_source(connection_or_uri)
33
+ if connection_or_uri.is_a? Mongo::MongoClient
34
+ connection_or_uri
35
+ else
36
+ Mongo::MongoClient.from_uri(connection_or_uri)
37
+ end
38
+ end
39
+
40
+ attr_reader :source
41
+
42
+ def initialize(options={})
43
+ @options = options
44
+ @source = self.class.connect_to_source(options['source'])
45
+ validate!
46
+ end
47
+
48
+ def targets
49
+ @options['targets']
50
+ end
51
+
52
+ def outlets
53
+ @options['outlets']
54
+ end
55
+
56
+ def validate!
57
+ raise Error, "Missing source" unless source
58
+ raise Error, "Missing outlets" unless outlets and not outlets.empty?
59
+ outlets.each do |outlet|
60
+ key = outlet['target']
61
+ target = (targets || {})[key]
62
+ raise Error, "Missing target '#{key}'" unless target
63
+ end
64
+ end
65
+
66
+ def db
67
+ @options['db'] || DEFAULT_DB
68
+ end
69
+ def db=(db)
70
+ @options['db'] = db
71
+ end
72
+
73
+ def service
74
+ @options['service'] || DEFAULT_SERVICE
75
+ end
76
+ def service=(service)
77
+ @options['service'] = service
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+
3
+ module MongoDelta
4
+ module Logging
5
+
6
+ def self.setup_logger
7
+ Logger.new(STDERR).tap do |logger|
8
+ logger.formatter = proc do |severity, datetime, progname, msg|
9
+ "#{datetime} - #{severity}: #{msg}\n"
10
+ end
11
+ end
12
+ end
13
+
14
+ def logger
15
+ @@logger ||= MongoDelta::Logging.setup_logger
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ module MongoDelta
2
+ module Outlet
3
+ class Base
4
+
5
+ attr_reader :target, :db, :collection, :options, :target_collection
6
+
7
+ def initialize(target, db, collection, options={})
8
+ @target = target
9
+ @db = db
10
+ @collection = collection
11
+ @options = options
12
+
13
+ @target_collection = target.db(options['target_db'] || db).collection(options['target_collection'] || collection)
14
+ end
15
+
16
+ def ns
17
+ "#{db}.#{collection}"
18
+ end
19
+
20
+ def handles?(db, collection, method)
21
+ @db == db and @collection == collection and respond_to?(method)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ require 'mongo_delta/outlet/base'
2
+
3
+ module MongoDelta
4
+ module Outlet
5
+ class Replicator < Base
6
+
7
+ def insert(document)
8
+ target_collection.save(document)
9
+ end
10
+
11
+ def remove(document)
12
+ target_collection.remove(document)
13
+ end
14
+
15
+ def update(selector, update)
16
+ target_collection.update(selector, update)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'mongo'
2
+
3
+ require 'mongo_delta/outlet/base'
4
+ require 'mongo_delta/outlet/replicator'
5
+
6
+ module MongoDelta
7
+ module Outlet
8
+
9
+ def self.from_options(config, options)
10
+ options = options.dup
11
+ klass = const_get(options.delete('outlet'))
12
+ target = fetch_target(config, options.delete('target'))
13
+ db = options.delete('db')
14
+ collection = options.delete('collection')
15
+ klass.new(target, db, collection, options)
16
+ end
17
+
18
+ private
19
+
20
+ def self.fetch_target(config, key)
21
+ @targets ||= {}
22
+ @targets[key] ||= Mongo::MongoClient.from_uri(config.targets[key])
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ require 'mongo'
2
+
3
+ require 'mongo_delta/agent'
4
+ require 'mongo_delta/outlet'
5
+
6
+ module MongoDelta
7
+ class Stream
8
+
9
+ include MongoDelta::Logging
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @tailer = Mongoriver::PersistentTailer.new([@config.source], :existing, @config.service, :db => @config.db)
14
+ @agent = MongoDelta::Agent.new
15
+ @stream = Mongoriver::Stream.new(@tailer, @agent)
16
+ setup_outlets
17
+ end
18
+
19
+ def run(ts=nil)
20
+ register_signal_handlers
21
+ logger.info "Starting stream"
22
+ @stream.run_forever(ts)
23
+ end
24
+
25
+ private
26
+
27
+ def setup_outlets
28
+ @config.outlets.each do |options|
29
+ outlet = MongoDelta::Outlet.from_options(@config, options)
30
+ logger.info "Registering #{options['outlet']} outlet for #{outlet.ns}"
31
+ @agent.outlets << outlet
32
+ end
33
+ end
34
+
35
+ def register_signal_handlers
36
+ logger.debug "Registering signal handlers"
37
+ %w[TERM INT USR2].each do |sig|
38
+ Signal.trap(sig) do
39
+ logger.info "Got SIG#{sig}. Preparing to exit..."
40
+ @stream.stop
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module MongoDelta
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'mongoriver'
2
+
3
+ module MongoDelta
4
+ end
5
+
6
+ require 'mongo_delta/configuration'
7
+ require 'mongo_delta/logging'
8
+ require 'mongo_delta/stream'
9
+ require 'mongo_delta/version'
@@ -0,0 +1,23 @@
1
+ $:.unshift(File.expand_path("lib", File.dirname(__FILE__)))
2
+
3
+ require 'mongo_delta/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ["Laszlo Bacsi"]
7
+ gem.email = ["lackac@lackac.hu"]
8
+ gem.description = %q{Streaming documents between MongoDB clusters}
9
+ gem.summary = %q{Replicate mongodb documents between clusters}
10
+ gem.homepage = "https://github.com/sspinc/mongo_delta"
11
+
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map {|f| File.basename(f)}
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.name = "mongo_delta"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = MongoDelta::VERSION
18
+
19
+ gem.add_runtime_dependency('mongo', '>= 1.7')
20
+ gem.add_runtime_dependency('mongoriver', '>= 0.3')
21
+
22
+ gem.add_development_dependency('rake')
23
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongo_delta
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Laszlo Bacsi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ prerelease: false
16
+ name: mongo
17
+ type: :runtime
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '1.7'
23
+ none: false
24
+ requirement: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ! '>='
27
+ - !ruby/object:Gem::Version
28
+ version: '1.7'
29
+ none: false
30
+ - !ruby/object:Gem::Dependency
31
+ prerelease: false
32
+ name: mongoriver
33
+ type: :runtime
34
+ version_requirements: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0.3'
39
+ none: false
40
+ requirement: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0.3'
45
+ none: false
46
+ - !ruby/object:Gem::Dependency
47
+ prerelease: false
48
+ name: rake
49
+ type: :development
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ none: false
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ none: false
62
+ description: Streaming documents between MongoDB clusters
63
+ email:
64
+ - lackac@lackac.hu
65
+ executables:
66
+ - mongo_delta
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .gitignore
71
+ - Gemfile
72
+ - LICENSE
73
+ - README.md
74
+ - Rakefile
75
+ - bin/mongo_delta
76
+ - config/mongo_delta.yml
77
+ - lib/mongo_delta.rb
78
+ - lib/mongo_delta/agent.rb
79
+ - lib/mongo_delta/cli.rb
80
+ - lib/mongo_delta/configuration.rb
81
+ - lib/mongo_delta/logging.rb
82
+ - lib/mongo_delta/outlet.rb
83
+ - lib/mongo_delta/outlet/base.rb
84
+ - lib/mongo_delta/outlet/replicator.rb
85
+ - lib/mongo_delta/stream.rb
86
+ - lib/mongo_delta/version.rb
87
+ - mongo_delta.gemspec
88
+ homepage: https://github.com/sspinc/mongo_delta
89
+ licenses: []
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 0
100
+ hash: -1239396457491943967
101
+ version: '0'
102
+ none: false
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ segments:
108
+ - 0
109
+ hash: -1239396457491943967
110
+ version: '0'
111
+ none: false
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 1.8.23
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: Replicate mongodb documents between clusters
118
+ test_files: []