apartment 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +5 -8
  4. data/Gemfile +1 -0
  5. data/README.md +79 -4
  6. data/apartment.gemspec +2 -2
  7. data/gemfiles/rails_3_2.gemfile +2 -0
  8. data/lib/apartment.rb +22 -2
  9. data/lib/apartment/adapters/abstract_adapter.rb +70 -16
  10. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +4 -9
  11. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +2 -13
  12. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +5 -16
  13. data/lib/apartment/adapters/mysql2_adapter.rb +8 -19
  14. data/lib/apartment/adapters/postgresql_adapter.rb +16 -41
  15. data/lib/apartment/adapters/sqlite3_adapter.rb +6 -3
  16. data/lib/apartment/elevators/first_subdomain.rb +1 -1
  17. data/lib/apartment/elevators/generic.rb +5 -3
  18. data/lib/apartment/version.rb +1 -1
  19. data/lib/generators/apartment/install/templates/apartment.rb +26 -1
  20. data/lib/tasks/apartment.rake +0 -1
  21. data/spec/adapters/jdbc_mysql_adapter_spec.rb +1 -1
  22. data/spec/adapters/jdbc_postgresql_adapter_spec.rb +1 -1
  23. data/spec/adapters/mysql2_adapter_spec.rb +2 -1
  24. data/spec/adapters/postgresql_adapter_spec.rb +1 -0
  25. data/spec/adapters/sqlite3_adapter_spec.rb +56 -0
  26. data/spec/apartment_spec.rb +2 -2
  27. data/spec/examples/connection_adapter_examples.rb +1 -1
  28. data/spec/examples/generic_adapter_custom_configuration_example.rb +90 -0
  29. data/spec/examples/generic_adapter_examples.rb +15 -15
  30. data/spec/examples/schema_adapter_examples.rb +25 -25
  31. data/spec/integration/apartment_rake_integration_spec.rb +4 -4
  32. data/spec/integration/query_caching_spec.rb +2 -2
  33. data/spec/spec_helper.rb +11 -0
  34. data/spec/support/apartment_helpers.rb +8 -2
  35. data/spec/support/setup.rb +3 -3
  36. data/spec/tasks/apartment_rake_spec.rb +11 -11
  37. data/spec/tenant_spec.rb +12 -12
  38. data/spec/unit/config_spec.rb +53 -23
  39. data/spec/unit/elevators/domain_spec.rb +4 -4
  40. data/spec/unit/elevators/first_subdomain_spec.rb +7 -2
  41. data/spec/unit/elevators/generic_spec.rb +19 -2
  42. data/spec/unit/elevators/host_hash_spec.rb +2 -2
  43. data/spec/unit/elevators/subdomain_spec.rb +6 -6
  44. data/spec/unit/migrator_spec.rb +1 -1
  45. data/spec/unit/reloader_spec.rb +2 -2
  46. metadata +11 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9cdae6ddf14e715ae80d9c932ca1d6425e6f56e1
4
- data.tar.gz: 8162a81d9eb38bf3992314a4d6fddad14bbc55a8
3
+ metadata.gz: 5edda0f57e98bfa6166bb6bd4fcea9368cdc2963
4
+ data.tar.gz: 8fdda9e1bbee41a48490ae0c61dc6bc18d5084c4
5
5
  SHA512:
6
- metadata.gz: 8683c9692ee16ece0b1876937e2ad79d25c057106c1f750e077386f5c0d04e822b5318baf250b10bcb0f9918bbddabdde6a53aaddd9637e9de0ccb9f33748208
7
- data.tar.gz: b76295e730aa02ac1f5a89b53459fed7499f70821bc0292321d2b2f7dc8fc036dc441995a25334f8207519102f5375063f6b99099902713db2c935f241e98238
6
+ metadata.gz: 7450954792e78f6bd27bb8c4a6414853331562554d027cb54703746f59f1f255d898b8966349640494c7574917924ab3faa690ef07d295791d50693c56b6a0eb
7
+ data.tar.gz: 9ffd46752338eb87341c0deb314395e0f8223beb5744a2ade9a632be6234f6ec68184a1ab01744b404978e11b25f80d03314645510cda7fb8875a1f86809c86d
@@ -1 +1 @@
1
- ruby-2.2
1
+ ruby-2.3
@@ -1,25 +1,22 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.0.0
5
- - 2.1.0
6
- - 2.2.0
7
- - jruby-19mode
8
- - jruby-head
4
+ - 2.1.9
5
+ - 2.2.4
6
+ - 2.3.1
7
+ - jruby-9.0.5.0
9
8
  gemfile:
10
9
  - gemfiles/rails_3_2.gemfile
11
10
  - gemfiles/rails_4_0.gemfile
12
11
  - gemfiles/rails_4_1.gemfile
13
12
  - gemfiles/rails_4_2.gemfile
14
- bundler_args: --without local --verbose
13
+ bundler_args: --without local
15
14
  before_install:
16
15
  - gem install bundler -v '> 1.5.0'
17
16
  env:
18
17
  RUBY_GC_MALLOC_LIMIT: 90000000
19
18
  RUBY_FREE_MIN: 200000
20
19
  matrix:
21
- allow_failures:
22
- - rvm: jruby-head
23
20
  exclude:
24
21
  - rvm: 2.2.0
25
22
  gemfile: gemfiles/rails_3_2.gemfile
data/Gemfile CHANGED
@@ -6,4 +6,5 @@ gem 'rails', '>= 3.1.2'
6
6
 
7
7
  group :local do
8
8
  gem 'pry'
9
+ gem 'guard-rspec', '~> 4.2'
9
10
  end
data/README.md CHANGED
@@ -8,14 +8,22 @@ Apartment provides tools to help you deal with multiple tenants in your Rails
8
8
  application. If you need to have certain data sequestered based on account or company,
9
9
  but still allow some data to exist in a common tenant, Apartment can help.
10
10
 
11
+ ## HELP!
12
+
13
+ In order to help drive the direction of development and clean up the codebase, we'd like to take a poll
14
+ on how people are currently using Apartment. If you can take 5 seconds (1 question) to answer
15
+ this poll, we'd greatly appreciated it.
16
+
17
+ [View Poll](http://www.poll-maker.com/poll391552x4Bfb41a9-15)
18
+
11
19
  ## Excessive Memory Issues on ActiveRecord 4.x
12
20
 
13
21
  > If you're noticing ever growing memory issues (ie growing with each tenant you add)
14
22
  > when using Apartment, that's because there's [an issue](https://github.com/rails/rails/issues/19578)
15
23
  > with how ActiveRecord maps Postgresql data types into AR data types.
16
- > This has been patched and will be release for AR 4.2.2. It's apparently hard
24
+ > This has been patched and will be released for AR 4.2.2. It's apparently hard
17
25
  > to backport to 4.1 unfortunately.
18
- > If you want to use this today, you can use our [4.2.1 patched version](https://github.com/influitive/rails/tree/v4.2.1.memfix) on our github account using the code sample below.
26
+ > If you're noticing high memory usage from ActiveRecord with Apartment please upgrade.
19
27
 
20
28
  ```ruby
21
29
  gem 'rails', '4.2.1', github: 'influitive/rails', tag: 'v4.2.1.memfix'
@@ -118,7 +126,16 @@ module MyApplication
118
126
  end
119
127
  ```
120
128
 
121
- If you want to exclude a domain, for example if you don't want your application to treate www like a subdomain, in an initializer in your application, you can set the following:
129
+ By default, the subdomain elevator assumes that the parent domain consists of two segments, e.g. 'example.com'. If this isn't the case, you can adjust the `tld_length` (top level domain length) configuration variable, which defaults to 1. For example, if you are using 'localhost' in development:
130
+ ```ruby
131
+ # config/initializers/apartment.rb
132
+ Apartment.configure do |config|
133
+ ...
134
+ config.tld_length = 0 if Rails.env == 'development'
135
+ end
136
+ ```
137
+
138
+ If you want to exclude a domain, for example if you don't want your application to treat www like a subdomain, in an initializer in your application, you can set the following:
122
139
 
123
140
  ```ruby
124
141
  # config/initializers/apartment/subdomain_exclusions.rb
@@ -127,6 +144,27 @@ Apartment::Elevators::Subdomain.excluded_subdomains = ['www']
127
144
 
128
145
  This functions much in the same way as Apartment.excluded_models. This example will prevent switching your tenant when the subdomain is www. Handy for subdomains like: "public", "www", and "admin" :)
129
146
 
147
+ **Switch on first subdomain**
148
+ To switch on the first subdomain, which analyzes the chain of subdomains of the request and switches to a tenant schema of the first name in the chain (e.g. owls.birds.animals.com would switch to "owl"). It can be used like so:
149
+
150
+ ```ruby
151
+ # application.rb
152
+ module MyApplication
153
+ class Application < Rails::Application
154
+ config.middleware.use 'Apartment::Elevators::FirstSubdomain'
155
+ end
156
+ end
157
+ ```
158
+
159
+ If you want to exclude a domain, for example if you don't want your application to treate www like a subdomain, in an initializer in your application, you can set the following:
160
+
161
+ ```ruby
162
+ # config/initializers/apartment/subdomain_exclusions.rb
163
+ Apartment::Elevators::FirstSubdomain.excluded_subdomains = ['www']
164
+ ```
165
+
166
+ This functions much in the same way as the Subdomain elevator.
167
+
130
168
  **Switch on domain**
131
169
  To switch based on full domain (excluding subdomains *ie 'www'* and top level domains *ie '.com'* ) use the following:
132
170
 
@@ -187,6 +225,16 @@ class MyCustomElevator < Apartment::Elevators::Generic
187
225
  end
188
226
  ```
189
227
 
228
+ ### Dropping Tenants
229
+
230
+ To drop tenants using Apartment, use the following command:
231
+
232
+ ```ruby
233
+ Apartment::Tenant.drop('tenant_name')
234
+ ```
235
+
236
+ When method is called, the schema is dropped and all data from itself will be lost. Be careful with this method.
237
+
190
238
  ## Config
191
239
 
192
240
  The following config options should be set up in a Rails initializer such as:
@@ -225,7 +273,7 @@ By default, ActiveRecord will use `"$user", public` as the default `schema_searc
225
273
  config.default_schema = "some_other_schema"
226
274
  ```
227
275
 
228
- With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Tenant` will return to this schema also
276
+ With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Tenant` will return to this schema as well.
229
277
 
230
278
  ## Persistent Schemas
231
279
  Apartment will normally just switch the `schema_search_path` whole hog to the one passed in. This can lead to problems if you want other schemas to always be searched as well. Enter `persistent_schemas`. You can configure a list of other schemas that will always remain in the search path, while the default gets swapped out:
@@ -377,6 +425,33 @@ and test environments. If you wish to turn this option off in production, you c
377
425
  config.prepend_environment = !Rails.env.production?
378
426
  ```
379
427
 
428
+ ## Tenants on different servers
429
+
430
+ You can store your tenants in different databases on one or more servers.
431
+ To do it, specify your `tenant_names` as a hash, keys being the actual tenant names,
432
+ values being a hash with the database configuration to use.
433
+
434
+ Example:
435
+
436
+ ```ruby
437
+ config.tenant_names = {
438
+ 'tenant1' => {
439
+ adapter: 'postgresql',
440
+ host: 'some_server',
441
+ port: 5555,
442
+ database: 'postgres' # this is not the name of the tenant's db
443
+ # but the name of the database to connect to, before creating the tenant's db
444
+ # mandatory in postgresql
445
+ }
446
+ }
447
+ # or using a lambda:
448
+ config.tenant_names = lambda do
449
+ Tenant.all.each_with_object({}) do |tenant, hash|
450
+ hash[tenant.name] = tenant.db_configuration
451
+ end
452
+ end
453
+ ```
454
+
380
455
  ## Delayed::Job
381
456
  ### Has been removed... See apartment-sidekiq for a better backgrounding experience
382
457
 
@@ -40,8 +40,8 @@ Gem::Specification.new do |s|
40
40
 
41
41
  s.add_development_dependency 'appraisal'
42
42
  s.add_development_dependency 'rake', '~> 0.9'
43
- s.add_development_dependency 'rspec-rails', '~> 2.14'
44
- s.add_development_dependency 'guard-rspec', '~> 4.2'
43
+ s.add_development_dependency 'rspec', '~> 3.4'
44
+ s.add_development_dependency 'rspec-rails', '~> 3.4'
45
45
  s.add_development_dependency 'capybara', '~> 2.0'
46
46
 
47
47
  if defined?(JRUBY_VERSION)
@@ -9,3 +9,5 @@ group :local do
9
9
  end
10
10
 
11
11
  gemspec :path => "../"
12
+
13
+ gem "test-unit"
@@ -24,9 +24,16 @@ module Apartment
24
24
  yield self if block_given?
25
25
  end
26
26
 
27
- # Be careful not to use `return` here so both Proc and lambda can be used without breaking
28
27
  def tenant_names
29
- @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
28
+ extract_tenant_config.keys.map(&:to_s)
29
+ end
30
+
31
+ def tenants_with_config
32
+ extract_tenant_config
33
+ end
34
+
35
+ def db_config_for(tenant)
36
+ (tenants_with_config[tenant] || connection_config).with_indifferent_access
30
37
  end
31
38
 
32
39
  # Whether or not db:migrate should also migrate tenants
@@ -96,6 +103,19 @@ module Apartment
96
103
  Apartment::Deprecation.warn "[Deprecation Warning] `use_postgresql_schemas=` is now deprecated, please use `use_schemas=`"
97
104
  self.use_schemas = to_use_or_not_to_use
98
105
  end
106
+
107
+ def extract_tenant_config
108
+ return {} unless @tenant_names
109
+ values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
110
+ unless values.is_a? Hash
111
+ values = values.each_with_object({}) do |tenant, hash|
112
+ hash[tenant] = connection_config
113
+ end
114
+ end
115
+ values.with_indifferent_access
116
+ rescue ActiveRecord::StatementInvalid
117
+ {}
118
+ end
99
119
  end
100
120
 
101
121
  # Exceptions
@@ -67,11 +67,12 @@ module Apartment
67
67
  # @param {String} tenant name
68
68
  #
69
69
  def drop(tenant)
70
- # Apartment.connection.drop_database note that drop_database will not throw an exception, so manually execute
71
- Apartment.connection.execute("DROP DATABASE #{environmentify(tenant)}" )
70
+ with_neutral_connection(tenant) do |conn|
71
+ drop_command(conn, tenant)
72
+ end
72
73
 
73
- rescue *rescuable_exceptions
74
- raise TenantNotFound, "The tenant #{environmentify(tenant)} cannot be found"
74
+ rescue *rescuable_exceptions => exception
75
+ raise_drop_tenant_error!(tenant, exception)
75
76
  end
76
77
 
77
78
  # Switch to a new tenant
@@ -125,7 +126,7 @@ module Apartment
125
126
  def process_excluded_models
126
127
  # All other models will shared a connection (at Apartment.connection_class) and we can modify at will
127
128
  Apartment.excluded_models.each do |excluded_model|
128
- excluded_model.constantize.establish_connection @config
129
+ process_excluded_model(excluded_model)
129
130
  end
130
131
  end
131
132
 
@@ -139,21 +140,38 @@ module Apartment
139
140
  #
140
141
  def seed_data
141
142
  # Don't log the output of seeding the db
142
- silence_stream(STDOUT){ load_or_abort(Apartment.seed_data_file) } if Apartment.seed_data_file
143
+ silence_warnings{ load_or_abort(Apartment.seed_data_file) } if Apartment.seed_data_file
143
144
  end
144
145
  alias_method :seed, :seed_data
145
146
 
146
147
  protected
147
148
 
149
+ def process_excluded_model(excluded_model)
150
+ excluded_model.constantize.establish_connection @config
151
+ end
152
+
153
+ def drop_command(conn, tenant)
154
+ # connection.drop_database note that drop_database will not throw an exception, so manually execute
155
+ conn.execute("DROP DATABASE #{environmentify(tenant)}")
156
+ end
157
+
158
+ class SeparateDbConnectionHandler < ::ActiveRecord::Base
159
+ end
160
+
148
161
  # Create the tenant
149
162
  #
150
163
  # @param {String} tenant Database name
151
164
  #
152
165
  def create_tenant(tenant)
153
- Apartment.connection.create_database( environmentify(tenant) )
166
+ with_neutral_connection(tenant) do |conn|
167
+ create_tenant_command(conn, tenant)
168
+ end
169
+ rescue *rescuable_exceptions => exception
170
+ raise_create_tenant_error!(tenant, exception)
171
+ end
154
172
 
155
- rescue *rescuable_exceptions
156
- raise TenantExists, "The tenant #{environmentify(tenant)} already exists."
173
+ def create_tenant_command(conn, tenant)
174
+ conn.create_database(environmentify(tenant))
157
175
  end
158
176
 
159
177
  # Connect to new tenant
@@ -163,9 +181,9 @@ module Apartment
163
181
  def connect_to_new(tenant)
164
182
  Apartment.establish_connection multi_tenantify(tenant)
165
183
  Apartment.connection.active? # call active? to manually check if this connection is valid
166
-
167
- rescue *rescuable_exceptions
168
- raise TenantNotFound, "The tenant #{environmentify(tenant)} cannot be found."
184
+ rescue *rescuable_exceptions => exception
185
+ Apartment::Tenant.reset if reset_on_connection_exception?
186
+ raise_connect_error!(tenant, exception)
169
187
  end
170
188
 
171
189
  # Prepend the environment if configured and the environment isn't already there
@@ -196,13 +214,21 @@ module Apartment
196
214
  end
197
215
 
198
216
  # Return a new config that is multi-tenanted
199
- #
200
- def multi_tenantify(tenant)
201
- @config.clone.tap do |config|
202
- config[:database] = environmentify(tenant)
217
+ # @param {String} tenant: Database name
218
+ # @param {Boolean} with_database: if true, use the actual tenant's db name
219
+ # if false, use the default db name from the db
220
+ def multi_tenantify(tenant, with_database = true)
221
+ db_connection_config(tenant).tap do |config|
222
+ if with_database
223
+ multi_tenantify_with_tenant_db_name(config, tenant)
224
+ end
203
225
  end
204
226
  end
205
227
 
228
+ def multi_tenantify_with_tenant_db_name(config, tenant)
229
+ config[:database] = environmentify(tenant)
230
+ end
231
+
206
232
  # Load a file or abort if it doesn't exists
207
233
  #
208
234
  def load_or_abort(file)
@@ -224,6 +250,34 @@ module Apartment
224
250
  def rescue_from
225
251
  []
226
252
  end
253
+
254
+ def db_connection_config(tenant)
255
+ Apartment.db_config_for(tenant).clone
256
+ end
257
+
258
+ # neutral connection is necessary whenever you need to create/remove a database from a server.
259
+ # example: when you use postgresql, you need to connect to the default postgresql database before you create your own.
260
+ def with_neutral_connection(tenant, &block)
261
+ SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
262
+ yield(SeparateDbConnectionHandler.connection)
263
+ SeparateDbConnectionHandler.connection.close
264
+ end
265
+
266
+ def reset_on_connection_exception?
267
+ false
268
+ end
269
+
270
+ def raise_drop_tenant_error!(tenant, exception)
271
+ raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{ exception.message }"
272
+ end
273
+
274
+ def raise_create_tenant_error!(tenant, exception)
275
+ raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{ exception.message }"
276
+ end
277
+
278
+ def raise_connect_error!(tenant, exception)
279
+ raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{ exception.message }"
280
+ end
227
281
  end
228
282
  end
229
283
  end
@@ -4,20 +4,15 @@ module Apartment
4
4
  module Adapters
5
5
  class AbstractJDBCAdapter < AbstractAdapter
6
6
 
7
- protected
7
+ private
8
8
 
9
- # Return a new config that is multi-tenanted
10
- #
11
- def multi_tenantify(database)
12
- @config.clone.tap do |config|
13
- config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(database)}"
14
- end
9
+ def multi_tenantify_with_tenant_db_name(config, tenant)
10
+ config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
15
11
  end
16
- private
17
12
 
18
13
  def rescue_from
19
14
  ActiveRecord::JDBCError
20
15
  end
21
16
  end
22
17
  end
23
- end
18
+ end
@@ -11,19 +11,8 @@ module Apartment
11
11
  module Adapters
12
12
  class JDBCMysqlAdapter < AbstractJDBCAdapter
13
13
 
14
- protected
15
-
16
- # Connect to new database
17
- # Abstract adapter will catch generic ActiveRecord error
18
- # Catch specific adapter errors here
19
- #
20
- # @param {String} database Database name
21
- #
22
- def connect_to_new(database)
23
- super
24
- rescue TenantNotFound
25
- Apartment::Tenant.reset
26
- raise TenantNotFound, "Cannot find database #{environmentify(database)}"
14
+ def reset_on_connection_exception?
15
+ true
27
16
  end
28
17
  end
29
18
  end
@@ -15,27 +15,16 @@ module Apartment
15
15
  # Default adapter when not using Postgresql Schemas
16
16
  class JDBCPostgresqlAdapter < PostgresqlAdapter
17
17
 
18
- protected
19
-
20
- def create_tenant(tenant)
21
- # There is a bug in activerecord-jdbcpostgresql-adapter (1.2.5) that will cause
22
- # an exception if no options are passed into the create_database call.
23
- Apartment.connection.create_database(environmentify(tenant), { :thisisahack => '' })
18
+ private
24
19
 
25
- rescue *rescuable_exceptions
26
- raise TenantExists, "The tenant #{environmentify(tenant)} already exists."
20
+ def multi_tenantify_with_tenant_db_name(config, tenant)
21
+ config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
27
22
  end
28
23
 
29
- # Return a new config that is multi-tenanted
30
- #
31
- def multi_tenantify(tenant)
32
- @config.clone.tap do |config|
33
- config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
34
- end
24
+ def create_tenant_command(conn, tenant)
25
+ conn.create_database(environmentify(tenant), { :thisisahack => '' })
35
26
  end
36
27
 
37
- private
38
-
39
28
  def rescue_from
40
29
  ActiveRecord::JDBCError
41
30
  end