mongar 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'linguistics'
7
+ gem 'mongo'
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "rspec", "~> 2.3.0"
13
+ gem "yard", "~> 0.6.0"
14
+ gem "bundler", "~> 1.0.0"
15
+ gem "jeweler", "~> 1.6.4"
16
+ gem "rcov", ">= 0"
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,36 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ bson (1.5.1)
5
+ diff-lcs (1.1.3)
6
+ git (1.2.5)
7
+ jeweler (1.6.4)
8
+ bundler (~> 1.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ linguistics (1.0.9)
12
+ mongo (1.5.1)
13
+ bson (= 1.5.1)
14
+ rake (0.9.2.2)
15
+ rcov (0.9.11)
16
+ rspec (2.3.0)
17
+ rspec-core (~> 2.3.0)
18
+ rspec-expectations (~> 2.3.0)
19
+ rspec-mocks (~> 2.3.0)
20
+ rspec-core (2.3.1)
21
+ rspec-expectations (2.3.0)
22
+ diff-lcs (~> 1.1.2)
23
+ rspec-mocks (2.3.0)
24
+ yard (0.6.8)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ bundler (~> 1.0.0)
31
+ jeweler (~> 1.6.4)
32
+ linguistics
33
+ mongo
34
+ rcov
35
+ rspec (~> 2.3.0)
36
+ yard (~> 0.6.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Greenview Data, 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.rdoc ADDED
@@ -0,0 +1,100 @@
1
+ = mongar
2
+
3
+ !! This is still fresh off the oven and hasn't been thoroughly tested yet in production. !!
4
+
5
+ Ruby GEM to facilitate replicating data from ActiveRecord (or any data represented by a set of Ruby classes) to MongoDB.
6
+
7
+ == Adding to a rails project
8
+
9
+ Put your Mongar config into config/mongar.rb (See Example Config below)
10
+
11
+ Put the following code into script/mongar.rb
12
+
13
+ require 'mongar'
14
+ mongar_config = File.join(Rails.root, 'config', 'mongar.rb')
15
+ mongar = eval(File.read(mongar_config))
16
+ mongar.run
17
+
18
+ Run it with rails runner (rails 3.x) or script/runner (rails 2.x) script/mongar.rb
19
+
20
+ == Example Config
21
+
22
+ Mongar.configure do
23
+ log_level :debug
24
+
25
+ mongo :default do
26
+ database 'mydb'
27
+ user 'mongouser'
28
+ password 'password'
29
+ host '127.0.0.1'
30
+ port 27017
31
+ end
32
+
33
+ mongo :otherdb do
34
+ database 'mydb'
35
+ user 'mongouser'
36
+ password 'password'
37
+
38
+ status_collection :statuses
39
+ end
40
+
41
+ replicate Domain => 'domains' do
42
+ use_mongodb :otherdb
43
+
44
+ full_refresh :every => 60.minutes
45
+
46
+ column :uri do
47
+ primary_index
48
+ transform :downcase
49
+ end
50
+
51
+ column :allow_anyone_to_anyone_policy
52
+ end
53
+
54
+ replicate Client do
55
+ column :id do
56
+ primary_index
57
+ end
58
+
59
+ column :name
60
+
61
+ column :employee_count do
62
+ transform do |value|
63
+ value.nil? ? 0 : value
64
+ end
65
+ end
66
+ end
67
+
68
+ replicate EmailAddress do
69
+ no_deleted_finder
70
+ set_updated_finder do |last_replicated_date|
71
+ find(:all, :conditions => ['something > ?', last_replicated_date])
72
+ end
73
+ set_created_finder do |last_replicated_date|
74
+ created_scope(last_replicated_date)
75
+ end
76
+
77
+ full_refresh :if => Proc.new do |last_replicated_date|
78
+ # class eval'ed code
79
+ any_changes_since?(last_replicated_date)
80
+ end
81
+
82
+ column :address
83
+ end
84
+ end
85
+
86
+ == Contributing to mongar
87
+
88
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
89
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
90
+ * Fork the project
91
+ * Start a feature/bugfix branch
92
+ * Commit and push until you are happy with your contribution
93
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
94
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
95
+
96
+ == Copyright
97
+
98
+ Copyright (c) 2011 Greenview Data, Inc. See LICENSE.txt for
99
+ further details.
100
+
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "mongar"
18
+ gem.homepage = "http://github.com/gdi/mongar"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Replicates data from ActiveRecord (or other Ruby data mapping class) to MongoDB}
21
+ gem.description = %Q{Replicates data from ActiveRecord (or other Ruby data mapping class) to MongoDB}
22
+ gem.email = "phil@greenviewdata.com"
23
+ gem.authors = ["Philippe Green"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'yard'
42
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.4
@@ -0,0 +1,42 @@
1
+ class Mongar
2
+ class Column
3
+ attr_accessor :name, :transformation, :is_indexed, :is_primary_index
4
+
5
+ def initialize(args = {})
6
+ self.name = args[:name]
7
+ self.transformation = nil
8
+ self.is_indexed = false
9
+ self.is_primary_index = false
10
+ end
11
+
12
+ def transform(proc_name = nil, &block)
13
+ self.transformation = lambda do
14
+ result = self
15
+ result = instance_exec(result, &block) if block_given?
16
+ result = result.send(proc_name) if proc_name
17
+ result
18
+ end
19
+ end
20
+
21
+ def transform_this(object)
22
+ return object unless transformation && transformation.is_a?(Proc)
23
+ object.instance_exec(&transformation)
24
+ end
25
+
26
+ def index
27
+ self.is_indexed = true
28
+ end
29
+
30
+ def primary_index
31
+ self.is_primary_index = true
32
+ end
33
+
34
+ def indexed?
35
+ self.is_indexed
36
+ end
37
+
38
+ def primary_index?
39
+ self.is_primary_index
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ class Mongar
2
+ module Logger
3
+ def info(log_message)
4
+ return unless @log_level && [:info, :debug].include?(@log_level)
5
+
6
+ puts "Info: #{log_message}"
7
+ end
8
+
9
+ def debug(log_message)
10
+ return unless @log_level && @log_level == :debug
11
+
12
+ puts "Debug: #{log_message}"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,93 @@
1
+ class Mongar::Mongo
2
+ class Collection
3
+ include Mongar::Logger
4
+
5
+ attr_reader :name, :log_level
6
+ attr_accessor :replica
7
+
8
+ def initialize(args = {})
9
+ @name = args[:name]
10
+ @replica = args[:replica]
11
+ @log_level = args[:log_level]
12
+ end
13
+
14
+ def mongodb
15
+ replica.mongodb
16
+ end
17
+
18
+ def connection
19
+ mongodb.connection!
20
+ end
21
+
22
+ def database
23
+ mongodb.db
24
+ end
25
+
26
+ def collection
27
+ database[name]
28
+ end
29
+
30
+ def status_collection
31
+ mongodb.status_collection_accessor
32
+ end
33
+
34
+ def last_replicated_at
35
+ status = status_collection.find_one({ :collection_name => name })
36
+ return Time.parse("1/1/1902 00:00:00") unless status && status['last_replicated_at']
37
+ status['last_replicated_at']
38
+ end
39
+
40
+ def last_replicated_at=(date)
41
+ info "Saving #{name} last_replicated_at to #{date}"
42
+ status_collection.update({ :collection_name => name },
43
+ { :collection_name => name, :last_replicated_at => date },
44
+ { :upsert => true })
45
+ end
46
+
47
+ def find(key)
48
+ debug "#{name}.find #{key.inspect}"
49
+ collection.find_one(key)
50
+ end
51
+
52
+ def create(document)
53
+ debug "#{name}.create #{document.inspect}"
54
+ !collection.insert(document).nil?
55
+ end
56
+
57
+ def delete(key)
58
+ debug "#{name}.delete #{key.inspect}"
59
+ collection.remove(key, { :safe => true })
60
+ end
61
+
62
+ def update(key, document)
63
+ debug "#{name}.update #{key.inspect} with #{document.inspect}"
64
+ collection.update(key, document, { :safe => true })
65
+ end
66
+
67
+ def create_or_update(key, document)
68
+ debug "#{name}.create_or_update #{key.inspect} with #{document.inspect}"
69
+
70
+ collection.update(key, document, {:upsert => true, :safe => true})
71
+ end
72
+
73
+ def mark_all_items_pending_deletion
74
+ info "Marking all items in #{name} for pending deletion"
75
+
76
+ collection.update({ '_id' => { '$exists' => true } }, { "$set" => { :pending_deletion => true } }, { :multi => true, :safe => true })
77
+ end
78
+
79
+ def delete_all_items_pending_deletion
80
+ info "Deleting all items in #{name} that are pending deletion"
81
+
82
+ collection.remove({ :pending_deletion => true }, { :safe => true })
83
+ end
84
+
85
+ [:create, :delete, :update, :create_or_update, :mark_all_items_pending_deletion, :delete_all_items_pending_deletion].each do |method_name|
86
+ define_method(:"#{method_name}!") do |*args|
87
+ result = self.send(method_name, *args)
88
+ raise StandardError, "#{method_name} returned #{result.inspect}" unless result == true || result['err'].nil?
89
+ result
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,51 @@
1
+ require 'mongo'
2
+ class Mongar
3
+ class Mongo
4
+ autoload :Collection, 'mongar/mongo/collection'
5
+
6
+ attr_reader :name
7
+
8
+ class << self
9
+ def databases
10
+ @databases ||= {}
11
+ end
12
+ end
13
+
14
+ def initialize(args = {})
15
+ args.each do |key, value|
16
+ instance_variable_set(:"@#{key}", value)
17
+ end
18
+
19
+ @host ||= '127.0.0.1'
20
+ @port ||= 27017
21
+ @status_collection ||= 'statuses'
22
+ end
23
+
24
+ [:database, :user, :password, :host, :port, :status_collection].each do |attr_name|
25
+ define_method(attr_name) do |*args|
26
+ val = args.first
27
+ return instance_variable_get(:"@#{attr_name}") if val.nil?
28
+ instance_variable_set(:"@#{attr_name}", val)
29
+ end
30
+ end
31
+
32
+ def connection
33
+ @connection = ::Mongo::Connection.new(host, port)
34
+ return @connection if self.user.nil? || @connection.authenticate(user, password)
35
+ @connection.close
36
+ @connection = nil
37
+ end
38
+
39
+ def connection!
40
+ connection or raise StandardError, "Could not establish '#{name}' MongoDB connection for #{database} at #{host}:#{port}"
41
+ end
42
+
43
+ def db
44
+ @db ||= connection!.db(database.to_s)
45
+ end
46
+
47
+ def status_collection_accessor
48
+ db[status_collection]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,153 @@
1
+ class Mongar
2
+ class Replica
3
+ include Mongar::Logger
4
+
5
+ attr_accessor :source, :destination, :log_level
6
+ attr_accessor :mongodb_name
7
+ attr_accessor :created_finder, :deleted_finder, :updated_finder
8
+ attr_accessor :columns
9
+
10
+ def initialize(args = {})
11
+ self.log_level = args[:log_level]
12
+ self.source = args[:source]
13
+ self.destination = args[:destination]
14
+ self.mongodb_name = args[:mongodb_name] || :default
15
+ self.columns = []
16
+
17
+ self.destination.replica = self if self.destination
18
+
19
+ self.deleted_finder = lambda do |last_replicated_at|
20
+ find_every_with_deleted(:conditions => ["deleted_at > ?", last_replicated_at])
21
+ end
22
+ self.created_finder = Proc.new do |last_replicated_at|
23
+ find(:all, :conditions => ["created_at > ? AND deleted_at IS NULL", last_replicated_at])
24
+ end
25
+ self.updated_finder = Proc.new do |last_replicated_at|
26
+ find(:all, :conditions => ["updated_at > ? AND deleted_at IS NULL", last_replicated_at])
27
+ end
28
+ end
29
+
30
+ def run
31
+ time = Time.now
32
+ if do_full_refresh?
33
+ info "Running full refresh on Replica #{source.to_s} to #{destination.name}"
34
+
35
+ destination.mark_all_items_pending_deletion!
36
+
37
+ run_sync_for([:created_or_updated], Time.parse('1/1/1902 00:00:00'))
38
+
39
+ destination.delete_all_items_pending_deletion!
40
+ else
41
+ last_replicated_at = destination.last_replicated_at
42
+
43
+ info "Running incremental replication on Replica #{source.to_s} to #{destination.name} from #{last_replicated_at}"
44
+
45
+ run_sync_for([:deleted, :created_or_updated, :updated], last_replicated_at)
46
+ end
47
+ destination.last_replicated_at = time
48
+ end
49
+
50
+ def run_sync_for(types, last_replicated_at)
51
+ # find deleted
52
+ find(:deleted, last_replicated_at).each do |deleted_item|
53
+ destination.delete! source_object_to_primary_key_hash(deleted_item)
54
+ end if types.include?(:deleted)
55
+
56
+ # find created
57
+ find(:created, last_replicated_at).each do |created_item|
58
+ destination.create! source_object_to_hash(created_item)
59
+ end if types.include?(:created)
60
+
61
+ # find created & updated
62
+ find(:created, last_replicated_at).each do |created_item|
63
+ destination.create_or_update! source_object_to_primary_key_hash(created_item), source_object_to_hash(created_item)
64
+ end if types.include?(:created_or_updated)
65
+
66
+ # find updated
67
+ find(:updated, last_replicated_at).each do |updated_item|
68
+ destination.update! source_object_to_primary_key_hash(updated_item), source_object_to_hash(updated_item)
69
+ end if types.include?(:updated)
70
+ end
71
+
72
+ def source_object_to_hash(object)
73
+ columns.inject({}) do |hash, column|
74
+ name = column.name.to_sym
75
+ hash[name] = column.transform_this(object.send(name))
76
+ hash
77
+ end
78
+ end
79
+
80
+ def source_object_to_primary_key_hash(object)
81
+ column = primary_index
82
+ { column.name => column.transform_this(object.send(column.name.to_sym)) }
83
+ end
84
+
85
+ def column(name, &block)
86
+ new_column = Mongar::Column.new(:name => name)
87
+ new_column.instance_eval(&block) if block_given?
88
+ self.columns << new_column
89
+ new_column
90
+ end
91
+
92
+ def primary_index
93
+ columns.find { |c| c.primary_index? }
94
+ end
95
+
96
+ def full_refresh(condition = nil)
97
+ return @full_refresh if condition.nil?
98
+
99
+ @full_refresh = if condition[:if]
100
+ condition[:if]
101
+ elsif condition[:every]
102
+ condition[:every]
103
+ else
104
+ raise StandardError, 'You must specify either :if or :every as a condition for full refresh'
105
+ end
106
+ end
107
+
108
+ def use_mongodb(name)
109
+ self.mongodb_name = name
110
+ end
111
+
112
+ def mongodb_name=(val)
113
+ @mongodb = nil
114
+ @mongodb_name = val
115
+ end
116
+
117
+ def mongodb
118
+ return nil unless mongodb_name
119
+ @mongodb ||= Mongar::Mongo.databases[mongodb_name]
120
+ end
121
+
122
+ def do_full_refresh?(last_replicated_time = nil)
123
+ last_replicated_time ||= destination.last_replicated_at
124
+
125
+ if @full_refresh.nil?
126
+ false
127
+ elsif @full_refresh.is_a?(Proc)
128
+ source.instance_exec last_replicated_time, &@full_refresh
129
+ elsif last_replicated_time.nil?
130
+ true
131
+ else
132
+ (Time.now - last_replicated_time) > @full_refresh
133
+ end
134
+ end
135
+
136
+ def find(type, last_replicated_time)
137
+ finder_function = self.send("#{type}_finder".to_sym)
138
+ return [] if finder_function.nil?
139
+ # execute the finder proc on the source object with an argument of the last replicated date/time
140
+ source.instance_exec(last_replicated_time, &finder_function) || []
141
+ end
142
+
143
+ [:deleted, :created, :updated].each do |finder_type|
144
+ define_method("set_#{finder_type}_finder".to_sym) do |&block|
145
+ self.send("#{finder_type}_finder=".to_sym, block)
146
+ end
147
+
148
+ define_method("no_#{finder_type}_finder") do
149
+ self.send("#{finder_type}_finder=".to_sym, nil)
150
+ end
151
+ end
152
+ end
153
+ end
data/lib/mongar.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'linguistics'
2
+
3
+ class Mongar
4
+ autoload :Replica, 'mongar/replica'
5
+ autoload :Column, 'mongar/column'
6
+ autoload :Mongo, 'mongar/mongo'
7
+ autoload :Logger, 'mongar/logger'
8
+
9
+ include Mongar::Logger
10
+
11
+ Linguistics.use :en
12
+
13
+ attr_accessor :replicas, :status_collection, :log_level
14
+
15
+ class << self
16
+ def configure &block
17
+ mongar = self.new
18
+ mongar.instance_eval(&block)
19
+ mongar
20
+ end
21
+ end
22
+
23
+ def initialize
24
+ self.replicas = []
25
+ end
26
+
27
+ def log_level(level = nil)
28
+ return @log_level if level.nil?
29
+ @log_level = level
30
+ end
31
+
32
+ def run
33
+ replicas.each do |replica|
34
+ replica.run
35
+ end
36
+ end
37
+
38
+ def replicate(what, &block)
39
+ if what.is_a?(Hash)
40
+ source = what.keys.first
41
+ destination = what.values.first
42
+ else
43
+ source = what
44
+ destination = what.to_s.downcase.en.plural
45
+ end
46
+
47
+ destination = Mongar::Mongo::Collection.new(:name => destination, :log_level => log_level)
48
+
49
+ self.replicas ||= []
50
+ replica = Replica.new(:source => source, :destination => destination, :log_level => log_level)
51
+ replica.instance_eval(&block)
52
+ self.replicas << replica
53
+ end
54
+
55
+ def mongo(name, &block)
56
+ mongo_db = Mongar::Mongo.new(:name => name)
57
+ mongo_db.instance_eval(&block)
58
+ Mongar::Mongo.databases[name] = mongo_db
59
+ mongo_db
60
+ end
61
+
62
+ def set_status_collection(val)
63
+ self.status_collection = val
64
+ end
65
+ end