mongo_delta 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []