apartment 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,3 +1,10 @@
1
+ # 0.10.0
2
+ * July 29, 2001
3
+
4
+ - Added better support for Delayed Job
5
+ - New config option that enables Delayed Job wrappers
6
+ - Note that DJ support uses a work-around in order to get queues stored in the public schema, not sure why it doesn't work out of the box, will look into it, until then, see documentation on queue'ng jobs
7
+
1
8
  # 0.9.2
2
9
  * July 4, 2011
3
10
 
data/README.md CHANGED
@@ -104,6 +104,30 @@ You can then migration your databases using the rake task:
104
104
  This basically invokes `Apartment::Database.migrate(#{db_name})` for each database name supplied
105
105
  from `Apartment.database_names`
106
106
 
107
+ ### Delayed::Job
108
+
109
+ In Apartment's current state, it doesn't seem to queue jobs properly using DJ. For whatever reason, DJ jobs are created in the current schema, even though the DJ
110
+ is part of the ignored models. I have to look into this further, but until then use `Apartment::Delayed::Job.enqueue` to ensure that queues are placed in the public schema
111
+
112
+ In order to make ActiveRecord models play nice with DJ and Apartment, include `Apartment::Delayed::Requirements` in any model that is being serialized by DJ. Also ensure
113
+ that a `database` attribute is set on this model *before* it is serialized, to ensure that when it is fetched again, it is done so in the proper Apartment db context. For example:
114
+
115
+ class SomeModel < ActiveRecord::Base
116
+ include Apartment::Delayed::Requirements
117
+ end
118
+
119
+ class SomeDJ
120
+
121
+ def initialize(model)
122
+ @model = model
123
+ @model.database = Apartment::Database.current_database
124
+ end
125
+
126
+ def perform
127
+ # do some stuff
128
+ end
129
+ end
130
+
107
131
  ## TODO
108
132
 
109
133
  * Cross-database associations
data/apartment.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'capybara', '1.0.0'
27
27
  s.add_development_dependency 'pg', '~> 0.11.0'
28
28
  s.add_development_dependency "silent-postgres", "~> 0.0.8"
29
+ s.add_development_dependency 'delayed_job', '~> 2.1.4'
29
30
  end
data/lib/apartment.rb CHANGED
@@ -41,6 +41,9 @@ module Apartment
41
41
  end
42
42
 
43
43
  module Delayed
44
+
45
+ autoload :Requirements, 'apartment/delayed_job/requirements'
46
+
44
47
  module Job
45
48
  autoload :Hooks, 'apartment/delayed_job/hooks'
46
49
  end
@@ -58,6 +61,9 @@ module Apartment
58
61
  # Raised when trying to create a schema that already exists
59
62
  class SchemaExists < ApartmentError; end
60
63
 
64
+ # Raised when an ActiveRecord object does not have the required database field on it
65
+ class DJSerializationError < ApartmentError; end
66
+
61
67
  end
62
68
 
63
69
  Apartment.configure do |config|
@@ -15,14 +15,15 @@ module Apartment
15
15
  @defaults = defaults
16
16
  end
17
17
 
18
- # Connect to db or schema, do stuff, reset (equivalent to a switch... do stuff... reset)
18
+ # Connect to db or schema, do stuff, switch back to previous db/schema (equivalent to a switch... do stuff... switch back)
19
19
  #
20
- # @param {String} database Database or schema to connect to
21
- def process(database)
22
- connect_to_new(database)
20
+ # @param {String?} database Database or schema to connect to, will reset if no database passed in
21
+ def process(database = nil)
22
+ current_db = current_database
23
+ switch(database)
23
24
  yield if block_given?
24
25
  ensure
25
- reset
26
+ switch(current_db)
26
27
  end
27
28
 
28
29
  # Create new postgres schema
@@ -65,48 +66,45 @@ module Apartment
65
66
  ActiveRecord::Base.connection.current_database
66
67
  end
67
68
 
68
- protected
69
-
70
- def create_schema
71
- # noop
72
- end
69
+ protected
70
+
71
+ def create_schema
72
+ # noop
73
+ end
74
+
75
+ def connect_to_new(database)
76
+ ActiveRecord::Base.establish_connection multi_tenantify(database)
77
+ end
73
78
 
74
- def connect_to_new(database)
75
- ActiveRecord::Base.establish_connection multi_tenantify(database)
79
+ def import_database_schema
80
+ load_or_abort("#{Rails.root}/db/schema.rb")
81
+ end
82
+
83
+ # Return a new config that is multi-tenanted
84
+ def multi_tenantify(database)
85
+ @config.clone.tap do |config|
86
+ config['database'].gsub!(Rails.env.to_s, "#{database}_#{Rails.env}")
76
87
  end
77
-
78
- def import_database_schema
79
- load_or_abort("#{Rails.root}/db/schema.rb")
80
- end
81
-
82
- # Return a new config that is multi-tenanted
83
- def multi_tenantify(database)
84
- @config.clone.tap do |config|
85
- config['database'].gsub!(Rails.env.to_s, "#{database}_#{Rails.env}")
86
- end
87
- end
88
-
89
- # Remove all non-alphanumeric characters
90
- def sanitize(database)
91
- database.gsub(/[\W]/,'')
92
- end
93
-
94
- # Whether or not to use postgresql schemas
95
- def using_schemas?
96
- false
97
- end
98
-
99
- def load_or_abort(file)
100
- if File.exists?(file)
101
- load(file)
102
- else
103
- abort %{#{file} doesn't exist yet}
104
- end
105
- end
88
+ end
106
89
 
107
- end
90
+ # Remove all non-alphanumeric characters
91
+ def sanitize(database)
92
+ database.gsub(/[\W]/,'')
93
+ end
108
94
 
95
+ # Whether or not to use postgresql schemas
96
+ def using_schemas?
97
+ false
98
+ end
109
99
 
110
-
100
+ def load_or_abort(file)
101
+ if File.exists?(file)
102
+ load(file)
103
+ else
104
+ abort %{#{file} doesn't exist yet}
105
+ end
106
+ end
107
+
108
+ end
111
109
  end
112
110
  end
@@ -14,7 +14,8 @@ module Apartment
14
14
  # Set schema path or connect to new db
15
15
  def connect_to_new(database)
16
16
  return ActiveRecord::Base.connection.schema_search_path = database if using_schemas?
17
- super
17
+
18
+ super # if !using_schemas? (implicit)
18
19
  rescue ActiveRecord::StatementInvalid => e
19
20
  raise SchemaNotFound, e
20
21
  end
@@ -28,27 +29,29 @@ module Apartment
28
29
 
29
30
  def reset
30
31
  return ActiveRecord::Base.connection.schema_search_path = @defaults[:schema_search_path] if using_schemas?
31
- super
32
+
33
+ super # if !using_schemas?
32
34
  end
33
35
 
34
36
  def current_database
35
37
  return ActiveRecord::Base.connection.schema_search_path if using_schemas?
36
- super
38
+
39
+ super # if !using_schemas?
37
40
  end
38
41
 
39
- protected
40
-
41
- def create_schema(database)
42
- reset
43
-
44
- ActiveRecord::Base.connection.execute("CREATE SCHEMA #{sanitize(database)}")
45
- rescue Exception => e
46
- raise SchemaExists, e
47
- end
42
+ protected
43
+
44
+ def create_schema(database)
45
+ reset
46
+
47
+ ActiveRecord::Base.connection.execute("CREATE SCHEMA #{sanitize(database)}")
48
+ rescue Exception => e
49
+ raise SchemaExists, e
50
+ end
48
51
 
49
- def using_schemas?
50
- Apartment.use_postgres_schemas
51
- end
52
+ def using_schemas?
53
+ Apartment.use_postgres_schemas
54
+ end
52
55
 
53
56
  end
54
57
 
@@ -0,0 +1,20 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ # Overriding Delayed Job's monkey_patch of ActiveRecord so that it works with Apartment
5
+ yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
6
+
7
+ def self.yaml_new(klass, tag, val)
8
+ Apartment::Database.process(val['database']) do
9
+ klass.find(val['attributes']['id'])
10
+ end
11
+ rescue ActiveRecord::RecordNotFound
12
+ raise Delayed::DeserializationError
13
+ end
14
+
15
+ def to_yaml_properties
16
+ ['@attributes', '@database'] # add in database attribute for serialization
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require 'delayed_job'
2
+ require 'apartment/delayed_job/active_record' # ensure that our AR hooks are loaded when queueing
3
+
4
+ module Apartment
5
+ module Delayed
6
+ module Job
7
+
8
+ # Will enqueue a job ensuring that it happens within the public schema
9
+ # This is a work-around due to the fact that DJ for some reason always
10
+ # queues its jobs in the current_schema, rather than the public schema
11
+ # as it is supposed to
12
+ def self.enqueue(payload_object, options = {})
13
+ Apartment::Database.process do
14
+ ::Delayed::Job.enqueue(payload_object, options)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -1,9 +1,11 @@
1
+ require 'apartment/delayed_job/enqueue'
2
+
1
3
  module Apartment
2
4
  module Delayed
3
5
  module Job
4
6
 
5
7
  # Before and after hooks for performing Delayed Jobs within a particular apartment database
6
- # Included these in your delayed jobs models and make sure provide a @database attr that will be serialized by DJ
8
+ # Include these in your delayed jobs models and make sure provide a @database attr that will be serialized by DJ
7
9
  module Hooks
8
10
 
9
11
  attr_accessor :database
@@ -0,0 +1,22 @@
1
+ require 'apartment/delayed_job/enqueue'
2
+
3
+ module Apartment
4
+ module Delayed
5
+
6
+ # Mix this module into any model that gets serialized by DJ
7
+ module Requirements
8
+ attr_accessor :database
9
+
10
+ def self.included(klass)
11
+ klass.after_find :set_database
12
+ end
13
+
14
+ private
15
+
16
+ def set_database
17
+ @database = Apartment::Database.current_database
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module Apartment
2
- VERSION = "0.9.2"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -18,7 +18,7 @@ module Dummy
18
18
  config.middleware.use 'Apartment::Elevators::Subdomain'
19
19
 
20
20
  # Custom directories with classes and modules you want to be autoloadable.
21
- # config.autoload_paths += %W(#{config.root}/extras)
21
+ config.autoload_paths += %W(#{config.root}/lib)
22
22
 
23
23
  # Only load the plugins named here, in the order given (default is alphabetical).
24
24
  # :all can be used as a placeholder for all plugins not explicitly named.
@@ -10,6 +10,21 @@ class CreateDummyModels < ActiveRecord::Migration
10
10
  t.datetime :birthdate
11
11
  t.string :sex
12
12
  end
13
+
14
+ create_table :delayed_jobs do |t|
15
+ t.integer "priority", :default => 0
16
+ t.integer "attempts", :default => 0
17
+ t.text "handler"
18
+ t.text "last_error"
19
+ t.datetime "run_at"
20
+ t.datetime "locked_at"
21
+ t.datetime "failed_at"
22
+ t.string "locked_by"
23
+ t.datetime "created_at"
24
+ t.datetime "updated_at"
25
+ end
26
+
27
+ add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
13
28
 
14
29
  end
15
30
 
@@ -17,6 +17,21 @@ ActiveRecord::Schema.define(:version => 20110613152810) do
17
17
  t.string "database"
18
18
  end
19
19
 
20
+ create_table "delayed_jobs", :force => true do |t|
21
+ t.integer "priority", :default => 0
22
+ t.integer "attempts", :default => 0
23
+ t.text "handler"
24
+ t.text "last_error"
25
+ t.datetime "run_at"
26
+ t.datetime "locked_at"
27
+ t.datetime "failed_at"
28
+ t.string "locked_by"
29
+ t.datetime "created_at"
30
+ t.datetime "updated_at"
31
+ end
32
+
33
+ add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
34
+
20
35
  create_table "users", :force => true do |t|
21
36
  t.string "name"
22
37
  t.datetime "birthdate"
@@ -0,0 +1,6 @@
1
+ class FakeDjClass
2
+
3
+ def perform
4
+ end
5
+
6
+ end
@@ -8,9 +8,11 @@ describe Apartment::Database do
8
8
 
9
9
  let(:config){ Apartment::Test.config['connections']['postgresql'].symbolize_keys }
10
10
  let(:database){ "some_new_database" }
11
+ let(:database2){ "yet_another_database" }
11
12
 
12
13
  before do
13
14
  ActiveRecord::Base.establish_connection config
15
+ Apartment::Test.load_schema # load the Rails schema in the public db schema
14
16
  Apartment::Database.stub(:config).and_return config # Use postgresql database config for this test
15
17
  @schema_search_path = ActiveRecord::Base.connection.schema_search_path
16
18
  end
@@ -63,20 +65,43 @@ describe Apartment::Database do
63
65
  end
64
66
 
65
67
  after do
66
- Apartment::Test.drop_schema(database)
68
+ Apartment::Test.drop_schema database
67
69
  end
68
70
 
69
71
  describe "#process" do
72
+
73
+ before do
74
+ Apartment::Database.create database2
75
+ end
76
+
77
+ after do
78
+ Apartment::Test.drop_schema database2
79
+ end
80
+
70
81
  it "should connect to new schema" do
71
82
  Apartment::Database.process(database) do
72
- ActiveRecord::Base.connection.schema_search_path.should == database
83
+ Apartment::Database.current_database.should == database
73
84
  end
74
85
  end
75
86
 
76
- it "should reset to public schema" do
87
+ it "should reset connection to the previous db" do
88
+ Apartment::Database.switch(database2)
77
89
  Apartment::Database.process(database)
78
- ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
90
+ Apartment::Database.current_database.should == database2
91
+ end
92
+
93
+ it "should reset to previous schema if database is nil" do
94
+ Apartment::Database.switch(database)
95
+ Apartment::Database.process
96
+ Apartment::Database.current_database.should == database
97
+ end
98
+
99
+ it "should set to public schema if database is nil" do
100
+ Apartment::Database.process do
101
+ Apartment::Database.current_database.should == @schema_search_path
102
+ end
79
103
  end
104
+
80
105
  end
81
106
 
82
107
  describe "#create" do
@@ -92,25 +117,67 @@ describe Apartment::Database do
92
117
 
93
118
  describe "#switch" do
94
119
 
120
+ let(:x){ rand(3) }
121
+
95
122
  it "should connect to new schema" do
96
123
  Apartment::Database.switch database
97
124
  ActiveRecord::Base.connection.schema_search_path.should == database
98
125
  end
99
126
 
100
- it "should ignore excluded models" do
127
+ it "should fail with invalid schema" do
128
+ expect {
129
+ Apartment::Database.switch('some_nonexistent_schema')
130
+ }.to raise_error Apartment::SchemaNotFound
131
+ end
132
+
133
+ context "creating models" do
134
+
135
+ before do
136
+ Apartment::Database.create database2
137
+ end
138
+
139
+ after do
140
+ Apartment::Test.drop_schema database2
141
+ end
142
+
143
+ it "should create a model instance in the current schema" do
144
+ Apartment::Database.switch database2
145
+ db2_count = User.count + x.times{ User.create }
146
+
147
+ Apartment::Database.switch database
148
+ db_count = User.count + x.times{ User.create }
149
+
150
+ Apartment::Database.switch database2
151
+ User.count.should == db2_count
152
+
153
+ Apartment::Database.switch database
154
+ User.count.should == db_count
155
+ end
156
+ end
157
+
158
+ context "with excluded models" do
159
+
101
160
  Apartment.configure do |config|
102
161
  config.excluded_models = [Company]
103
162
  end
104
163
 
105
- Apartment::Database.switch database
106
- Company.connection.schema_search_path.should == @schema_search_path
164
+ it "should ignore excluded models" do
165
+ Apartment::Database.switch database
166
+ Company.connection.schema_search_path.should == @schema_search_path
167
+ end
168
+
169
+ it "should create excluded models in public schema" do
170
+ Apartment::Database.reset # ensure we're on public schema
171
+ count = Company.count + x.times{ Company.create }
172
+
173
+ Apartment::Database.switch database
174
+ x.times{ Company.create }
175
+ Company.count.should == count + x
176
+ Apartment::Database.reset
177
+ Company.count.should == count + x
178
+ end
107
179
  end
108
180
 
109
- it "should fail with invalid schema" do
110
- expect {
111
- Apartment::Database.switch('some_nonexistent_schema')
112
- }.to raise_error Apartment::SchemaNotFound
113
- end
114
181
  end
115
182
 
116
183
  describe "#current_database" do
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'delayed_job'
3
+ Delayed::Worker.guess_backend
4
+
5
+ describe Apartment::Delayed do
6
+
7
+ # See apartment.yml file in dummy app config
8
+
9
+ let(:config){ Apartment::Test.config['connections']['postgresql'].symbolize_keys }
10
+ let(:database){ "some_new_database" }
11
+ let(:database2){ "another_db" }
12
+
13
+ before do
14
+ ActiveRecord::Base.establish_connection config
15
+ Apartment::Test.load_schema # load the Rails schema in the public db schema
16
+ Apartment::Database.stub(:config).and_return config # Use postgresql database config for this test
17
+ @schema_search_path = ActiveRecord::Base.connection.schema_search_path
18
+
19
+ Apartment.configure do |config|
20
+ config.use_postgres_schemas = true
21
+ end
22
+
23
+ Apartment::Database.create database
24
+ Apartment::Database.create database2
25
+ end
26
+
27
+ after do
28
+ Apartment::Test.drop_schema database
29
+ Apartment::Test.drop_schema database2
30
+ Apartment::Test.reset
31
+ end
32
+
33
+ describe Apartment::Delayed::Job do
34
+ context "#enqueue" do
35
+
36
+ before do
37
+ Apartment::Database.reset
38
+ end
39
+
40
+ it "should queue up jobs in the public schema" do
41
+ dj_count = Delayed::Job.count
42
+ Apartment::Database.switch database
43
+ Apartment::Delayed::Job.enqueue FakeDjClass.new
44
+ Apartment::Database.reset
45
+
46
+ Delayed::Job.count.should == dj_count + 1
47
+ end
48
+
49
+ it "should not queue jobs in the current schema" do
50
+ Apartment::Database.switch database
51
+ expect {
52
+ Apartment::Delayed::Job.enqueue FakeDjClass.new
53
+ }.to_not change(Delayed::Job, :count) # because we will still be on the `database` schema, not public
54
+ end
55
+ end
56
+ end
57
+
58
+ describe Apartment::Delayed::Requirements do
59
+
60
+ before do
61
+ Apartment::Database.switch database
62
+ User.send(:include, Apartment::Delayed::Requirements)
63
+ User.create
64
+ end
65
+
66
+ it "should initialize a database attribute on a class" do
67
+ user = User.first
68
+ user.database.should == database
69
+ end
70
+
71
+ it "should not overwrite any previous after_initialize declarations" do
72
+ User.class_eval do
73
+ after_find :set_name
74
+
75
+ def set_name
76
+ self.name = "Some Name"
77
+ end
78
+ end
79
+
80
+ user = User.first
81
+ user.database.should == database
82
+ user.name.should == "Some Name"
83
+ end
84
+
85
+ context "serialization" do
86
+ it "should serialize the proper database attribute" do
87
+ user_yaml = User.first.to_yaml
88
+ Apartment::Database.switch database2
89
+ user = YAML.load user_yaml
90
+ user.database.should == database
91
+ end
92
+ end
93
+ end
94
+
95
+ end
@@ -14,6 +14,10 @@ module Apartment
14
14
  ActiveRecord::Base.connection.execute("CREATE SCHEMA #{schema}")
15
15
  end
16
16
 
17
+ def self.load_schema
18
+ load "#{Rails.root}/db/schema.rb"
19
+ end
20
+
17
21
  def self.in_database(db)
18
22
  Apartment::Database.switch db
19
23
  yield if block_given?
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: apartment
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.9.2
5
+ version: 0.10.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Ryan Brunner
@@ -91,6 +91,17 @@ dependencies:
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: *id007
94
+ - !ruby/object:Gem::Dependency
95
+ name: delayed_job
96
+ requirement: &id008 !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 2.1.4
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: *id008
94
105
  description: Apartment allows Rails applications to deal with database multitenancy
95
106
  email:
96
107
  - ryan@ryanbrunner.com
@@ -115,7 +126,10 @@ files:
115
126
  - lib/apartment/adapters/mysql_adapter.rb
116
127
  - lib/apartment/adapters/postgresql_adapter.rb
117
128
  - lib/apartment/database.rb
129
+ - lib/apartment/delayed_job/active_record.rb
130
+ - lib/apartment/delayed_job/enqueue.rb
118
131
  - lib/apartment/delayed_job/hooks.rb
132
+ - lib/apartment/delayed_job/requirements.rb
119
133
  - lib/apartment/elevators/subdomain.rb
120
134
  - lib/apartment/migrator.rb
121
135
  - lib/apartment/railtie.rb
@@ -149,6 +163,7 @@ files:
149
163
  - spec/dummy/db/schema.rb
150
164
  - spec/dummy/db/seeds.rb
151
165
  - spec/dummy/db/test.sqlite3
166
+ - spec/dummy/lib/fake_dj_class.rb
152
167
  - spec/dummy/log/development.log
153
168
  - spec/dummy/log/production.log
154
169
  - spec/dummy/log/server.log
@@ -161,6 +176,7 @@ files:
161
176
  - spec/integration/adapters/postgresql_integration_spec.rb
162
177
  - spec/integration/apartment_rake_integration_spec.rb
163
178
  - spec/integration/database_integration_spec.rb
179
+ - spec/integration/delayed_job_integration_spec.rb
164
180
  - spec/integration/middleware/subdomain_elevator_spec.rb
165
181
  - spec/integration/setup_spec.rb
166
182
  - spec/spec_helper.rb
@@ -186,7 +202,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
186
202
  requirements:
187
203
  - - ">="
188
204
  - !ruby/object:Gem::Version
189
- hash: 1126220868121977386
205
+ hash: -3391988953711779455
190
206
  segments:
191
207
  - 0
192
208
  version: "0"
@@ -195,7 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
211
  requirements:
196
212
  - - ">="
197
213
  - !ruby/object:Gem::Version
198
- hash: 1126220868121977386
214
+ hash: -3391988953711779455
199
215
  segments:
200
216
  - 0
201
217
  version: "0"