mongoid_collection_snapshot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem "mongoid_slug", ">= 0.8.2"
6
+
7
+ # Add dependencies to develop your gem here.
8
+ # Include everything needed to run rake, tests, features, etc.
9
+ group :development do
10
+ gem "mongoid", "~> 2.0.0"
11
+ gem "bson_ext", "~> 1.3.0"
12
+ gem "rspec", "~> 2.5.0"
13
+ gem "jeweler", "~> 1.5.2"
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Art.sy
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,80 @@
1
+ Mongoid Collection Snapshot
2
+ ===========================
3
+
4
+ Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM.
5
+
6
+ Example:
7
+ --------
8
+
9
+ Suppose that you have a Mongoid model called `Artwork`, stored
10
+ in a MongoDB collection called `artworks` and the underlying documents
11
+ look something like:
12
+
13
+ { name: 'Flowers', artist: 'Andy Warhol', price: 3000000 }
14
+
15
+ From time to time, your system runs a map/reduce job to compute the
16
+ average price of each artist's works, resulting in a collection called
17
+ `artist_average_price` that contains documents that look like:
18
+
19
+ { _id: { artist: 'Andy Warhol'}, value: { price: 1500000 } }
20
+
21
+ If your system wants to maintain and use this average price data, it has
22
+ to do so at the level of raw MongoDB operations, since
23
+ map/reduce result documents don't map well to models in Mongoid.
24
+ Furthermore, even though map/reduce jobs can take some time to run, you probably
25
+ want the entire `artist_average_price` collection populated atomically
26
+ from the point of view of your system, since otherwise you don't ever
27
+ know the state of the data in the collection - you could access it in
28
+ the middle of a map/reduce and get partial, incorrect results.
29
+
30
+ mongoid_collection_snapshot solves this problem by providing an atomic
31
+ view of collections of data like map/reduce results that live outside
32
+ of Mongoid.
33
+
34
+ In the example above, we'd set up our average artist price collection like:
35
+
36
+ class AverageArtistPrice
37
+ include Mongoid::CollectionSnapshot
38
+
39
+ def build
40
+ map = <<-EOS
41
+ function() {
42
+ emit({artist: this['artist']}, {count: 1, sum: this['price']})
43
+ }
44
+ EOS
45
+
46
+ reduce = <<-EOS
47
+ function(key, values) {
48
+ var sum = 0;
49
+ var count = 0;
50
+ values.forEach(function(value) {
51
+ sum += value['price'];
52
+ count += value['count'];
53
+ });
54
+ return({count: count, sum: sum});
55
+ }
56
+ EOS
57
+
58
+ Artwork.collection.map_reduce(map, reduce, :out => collection_snapshot.name)
59
+ end
60
+
61
+ def average_price(artist)
62
+ doc = collection_snapshot.findOne({'_id.artist': artist})
63
+ doc['value']['sum']/doc['value']['count']
64
+ end
65
+ end
66
+
67
+ Now, if you want
68
+ to schedule a recomputation, just call `AverageArtistPrice.create`. The latest
69
+ snapshot is always available as `AverageArtistPrice.latest`, so you can write
70
+ code like:
71
+
72
+ warhol_expected_price = AverageArtistPrice.latest.average_price('Andy Warhol')
73
+
74
+ And always be sure that you'll never be looking at partial results. The only
75
+ thing you need to do to hook into mongoid_collection_snapshot is implement the
76
+ method `build`, which populates the collection snapshot and any indexes you need.
77
+
78
+ By default, mongoid_collection_snapshot maintains the most recent two snapshots
79
+ computed any given time.
80
+
data/Rakefile ADDED
@@ -0,0 +1,35 @@
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 = "mongoid_collection_snapshot"
18
+ gem.homepage = "http://github.com/aaw/mongoid_collection_snapshot"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM}
21
+ gem.description = %Q{Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM}
22
+ gem.email = "aaron.windsor@gmail.com"
23
+ gem.authors = ["Aaron Windsor"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,46 @@
1
+ module Mongoid::CollectionSnapshot
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ require 'mongoid_slug'
6
+
7
+ include Mongoid::Document
8
+ include Mongoid::Timestamps::Created
9
+ include Mongoid::Slug
10
+
11
+ field :workspace_basename, default: 'snapshot'
12
+ slug :workspace_basename, as: :workspace_slug
13
+
14
+ field :max_collection_snapshot_instances, default: 2
15
+
16
+ before_save :build
17
+ after_save :ensure_at_most_two_instances_exist
18
+ before_destroy :drop_snapshot_collection
19
+ end
20
+
21
+ module ClassMethods
22
+ def latest
23
+ order_by([[:created_at, :desc]]).first
24
+ end
25
+ end
26
+
27
+ def collection_snapshot
28
+ Mongoid.master.collection("#{self.collection.name}.#{workspace_slug}")
29
+ end
30
+
31
+ def drop_snapshot_collection
32
+ collection_snapshot.drop
33
+ end
34
+
35
+ # Since we should always be using the latest instance of this class, this method is
36
+ # called after each save - making sure only at most two instances exists should be
37
+ # sufficient to ensure that this data can be rebuilt live without corrupting any
38
+ # existing computations that might have a handle to the previous "latest" instance.
39
+ def ensure_at_most_two_instances_exist
40
+ all_instances = self.class.order_by([[:created_at, :desc]]).to_a
41
+ if all_instances.length > self.max_collection_snapshot_instances
42
+ all_instances[self.max_collection_snapshot_instances..-1].each { |instance| instance.destroy }
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,68 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{mongoid_collection_snapshot}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Aaron Windsor"]
12
+ s.date = %q{2011-09-02}
13
+ s.description = %q{Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM}
14
+ s.email = %q{aaron.windsor@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "Gemfile",
21
+ "LICENSE.txt",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "lib/mongoid_collection_snapshot.rb",
26
+ "mongoid_collection_snapshot.gemspec",
27
+ "spec/models/artwork.rb",
28
+ "spec/models/average_artist_price.rb",
29
+ "spec/mongoid/collection_snapshot_spec.rb",
30
+ "spec/spec_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/aaw/mongoid_collection_snapshot}
33
+ s.licenses = ["MIT"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.6.2}
36
+ s.summary = %q{Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM}
37
+ s.test_files = [
38
+ "spec/models/artwork.rb",
39
+ "spec/models/average_artist_price.rb",
40
+ "spec/mongoid/collection_snapshot_spec.rb",
41
+ "spec/spec_helper.rb"
42
+ ]
43
+
44
+ if s.respond_to? :specification_version then
45
+ s.specification_version = 3
46
+
47
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
48
+ s.add_runtime_dependency(%q<mongoid_slug>, [">= 0.8.2"])
49
+ s.add_development_dependency(%q<mongoid>, ["~> 2.0.0"])
50
+ s.add_development_dependency(%q<bson_ext>, ["~> 1.3.0"])
51
+ s.add_development_dependency(%q<rspec>, ["~> 2.5.0"])
52
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
53
+ else
54
+ s.add_dependency(%q<mongoid_slug>, [">= 0.8.2"])
55
+ s.add_dependency(%q<mongoid>, ["~> 2.0.0"])
56
+ s.add_dependency(%q<bson_ext>, ["~> 1.3.0"])
57
+ s.add_dependency(%q<rspec>, ["~> 2.5.0"])
58
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
59
+ end
60
+ else
61
+ s.add_dependency(%q<mongoid_slug>, [">= 0.8.2"])
62
+ s.add_dependency(%q<mongoid>, ["~> 2.0.0"])
63
+ s.add_dependency(%q<bson_ext>, ["~> 1.3.0"])
64
+ s.add_dependency(%q<rspec>, ["~> 2.5.0"])
65
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
66
+ end
67
+ end
68
+
@@ -0,0 +1,7 @@
1
+ class Artwork
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+ field :artist
6
+ field :price
7
+ end
@@ -0,0 +1,31 @@
1
+ class AverageArtistPrice
2
+ include Mongoid::CollectionSnapshot
3
+
4
+ def build
5
+ map = <<-EOS
6
+ function() {
7
+ emit({artist: this['artist']}, {count: 1, sum: this['price']})
8
+ }
9
+ EOS
10
+
11
+ reduce = <<-EOS
12
+ function(key, values) {
13
+ var sum = 0;
14
+ var count = 0;
15
+ values.forEach(function(value) {
16
+ sum += value['sum'];
17
+ count += value['count'];
18
+ });
19
+ return({count: count, sum: sum});
20
+ }
21
+ EOS
22
+
23
+ Artwork.collection.map_reduce(map, reduce, :out => collection_snapshot.name)
24
+ end
25
+
26
+ def average_price(artist)
27
+ doc = collection_snapshot.find_one({'_id.artist' => artist})
28
+ doc['value']['sum']/doc['value']['count']
29
+ end
30
+
31
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ module Mongoid
4
+ describe CollectionSnapshot do
5
+
6
+ context "creating a snapshot" do
7
+
8
+ let!(:flowers) { Artwork.create(:name => 'Flowers', :artist => 'Andy Warhol', :price => 3000000) }
9
+ let!(:guns) { Artwork.create(:name => 'Guns', :artist => 'Andy Warhol', :price => 1000000) }
10
+ let!(:vinblastine) { Artwork.create(:name => 'Vinblastine', :artist => 'Damien Hirst', :price => 1500000) }
11
+
12
+ it "returns nil if no snapshot has been created" do
13
+ AverageArtistPrice.latest.should be_nil
14
+ end
15
+ it "runs the build method on creation" do
16
+ snapshot = AverageArtistPrice.create
17
+ snapshot.average_price('Andy Warhol').should == 2000000
18
+ snapshot.average_price('Damien Hirst').should == 1500000
19
+ end
20
+ it "returns the most recent snapshot through the latest methods" do
21
+ first = AverageArtistPrice.create
22
+ first.should == AverageArtistPrice.latest
23
+ # "latest" only works up to a resolution of 1 second since it relies on Mongoid::Timestamp. But this
24
+ # module is meant to snapshot long-running collection creation, so if you need a resolution of less
25
+ # than a second for "latest" then you're probably using the wrong gem. In tests, sleeping for a second
26
+ # makes sure we get what we expect.
27
+ sleep(1)
28
+ second = AverageArtistPrice.create
29
+ AverageArtistPrice.latest.should == second
30
+ sleep(1)
31
+ third = AverageArtistPrice.create
32
+ AverageArtistPrice.latest.should == third
33
+ end
34
+ it "should only maintain at most two of the latest snapshots to support its calculations" do
35
+ 10.times do
36
+ AverageArtistPrice.create
37
+ AverageArtistPrice.count.should <= 2
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec'
4
+
5
+ require 'mongoid'
6
+
7
+ Mongoid.configure do |config|
8
+ name = "mongoid_collection_snapshot_test"
9
+ config.master = Mongo::Connection.new.db(name)
10
+ config.logger = Logger.new('/dev/null')
11
+ end
12
+
13
+ require File.expand_path("../../lib/mongoid_collection_snapshot", __FILE__)
14
+ Dir["#{File.dirname(__FILE__)}/models/**/*.rb"].each { |f| require f }
15
+
16
+ Rspec.configure do |c|
17
+ c.before(:each) do
18
+ Mongoid.master.collections.select {|c| c.name !~ /system/ }.each(&:drop)
19
+ end
20
+ c.after(:all) do
21
+ Mongoid.master.command({'dropDatabase' => 1})
22
+ end
23
+ end
24
+
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid_collection_snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Aaron Windsor
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-02 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mongoid_slug
17
+ requirement: &75871620 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *75871620
26
+ - !ruby/object:Gem::Dependency
27
+ name: mongoid
28
+ requirement: &75871010 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *75871010
37
+ - !ruby/object:Gem::Dependency
38
+ name: bson_ext
39
+ requirement: &75870610 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 1.3.0
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *75870610
48
+ - !ruby/object:Gem::Dependency
49
+ name: rspec
50
+ requirement: &75796300 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 2.5.0
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *75796300
59
+ - !ruby/object:Gem::Dependency
60
+ name: jeweler
61
+ requirement: &75795230 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 1.5.2
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *75795230
70
+ description: Easy maintenence of collections of processed data in MongoDB with the
71
+ Mongoid ODM
72
+ email: aaron.windsor@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files:
76
+ - LICENSE.txt
77
+ - README.md
78
+ files:
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - VERSION
84
+ - lib/mongoid_collection_snapshot.rb
85
+ - mongoid_collection_snapshot.gemspec
86
+ - spec/models/artwork.rb
87
+ - spec/models/average_artist_price.rb
88
+ - spec/mongoid/collection_snapshot_spec.rb
89
+ - spec/spec_helper.rb
90
+ has_rdoc: true
91
+ homepage: http://github.com/aaw/mongoid_collection_snapshot
92
+ licenses:
93
+ - MIT
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ segments:
105
+ - 0
106
+ hash: 522934671
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 1.6.2
116
+ signing_key:
117
+ specification_version: 3
118
+ summary: Easy maintenence of collections of processed data in MongoDB with the Mongoid
119
+ ODM
120
+ test_files:
121
+ - spec/models/artwork.rb
122
+ - spec/models/average_artist_price.rb
123
+ - spec/mongoid/collection_snapshot_spec.rb
124
+ - spec/spec_helper.rb