motel-activerecord 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +225 -0
- data/VERSION +1 -0
- data/lib/motel-activerecord.rb +10 -0
- data/lib/motel/connection_adapters.rb +3 -0
- data/lib/motel/connection_adapters/connection_handler.rb +96 -0
- data/lib/motel/connection_adapters/connection_specification.rb +2 -0
- data/lib/motel/connection_adapters/connection_specification/resolver.rb +92 -0
- data/lib/motel/errors.rb +28 -0
- data/lib/motel/lobby.rb +47 -0
- data/lib/motel/manager.rb +69 -0
- data/lib/motel/multi_tenant.rb +53 -0
- data/lib/motel/railtie.rb +63 -0
- data/lib/motel/sources.rb +3 -0
- data/lib/motel/sources/database.rb +120 -0
- data/lib/motel/sources/default.rb +54 -0
- data/lib/motel/sources/redis.rb +80 -0
- data/lib/motel/version.rb +6 -0
- data/spec/lib/motel/connection_adapters/connection_handler_spec.rb +184 -0
- data/spec/lib/motel/connection_adapters/connection_specification/resolver_spec.rb +120 -0
- data/spec/lib/motel/lobby_spec.rb +155 -0
- data/spec/lib/motel/manager_spec.rb +238 -0
- data/spec/lib/motel/multi_tenant_spec.rb +169 -0
- data/spec/lib/motel/sources/database_spec.rb +246 -0
- data/spec/lib/motel/sources/default_spec.rb +167 -0
- data/spec/lib/motel/sources/redis_spec.rb +188 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/tmp/foo.sqlite3 +0 -0
- data/spec/tmp/tenants.sqlite3 +0 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4d99bb0326610a2417b22820a6f19fe41267d345
|
4
|
+
data.tar.gz: 5758e9d8179ff47709fe4b3f4633e55842a386ed
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 19f4d633c32fe7a15744f7c3c3599a2adb43c5e04bd1f6be0ce7e1c6d8f12ecf1303707c68f8b58bd5e8ce59fc6906a633b336cfafbc59d60771dc4a3f3ce9f3
|
7
|
+
data.tar.gz: e1900055fe03cb927f8daaf1d6ad4eaf887fe941dbacbe6e371f2ffcd0d6c2e7d0db139ebcf0e69740f7b97d06032a3bad297b24fbbb7c0f3588662583ff6ee7
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
Motel ActiveRecord
|
2
|
+
===================
|
3
|
+
|
4
|
+
Motel is a gem that adds functionality to ActiveRecord to use
|
5
|
+
connections to multiple databases, one for each tenant.
|
6
|
+
|
7
|
+
# Features
|
8
|
+
|
9
|
+
* Adds multi-tenant functionality to ActiveRecord.
|
10
|
+
* Multiple databases, one for each tenant.
|
11
|
+
* Tenant connection details are stored keying them by the name on a database or redis server.
|
12
|
+
* Use with or without Rails.
|
13
|
+
|
14
|
+
# Installing
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem install motel-activerecord
|
18
|
+
```
|
19
|
+
|
20
|
+
or add the following line to Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'motel-activerecord'
|
24
|
+
```
|
25
|
+
|
26
|
+
and run `bundle install` from your shell.
|
27
|
+
|
28
|
+
# Supported Ruby and Rails versions
|
29
|
+
The gem motel-activerecord supports MRI Ruby 2.0 or greater and Rails 4.0 or greater.
|
30
|
+
|
31
|
+
# Configuration
|
32
|
+
|
33
|
+
## Use with Rails
|
34
|
+
|
35
|
+
In your app/application.rb file write this:
|
36
|
+
|
37
|
+
### Specifying database as a source of tenants
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
config.motel.tenants_source_configurations = {
|
41
|
+
source: :database,
|
42
|
+
source_spec: { adapter: 'sqlite3', database: 'db/tenants.sqlite3' },
|
43
|
+
table_name: 'tenant'
|
44
|
+
}
|
45
|
+
```
|
46
|
+
|
47
|
+
You can specify the source by providing a hash of the
|
48
|
+
connection specification. Example:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
source_spec: {adapter: 'sqlite3', database: 'db/tenants.sqlite3'}
|
52
|
+
```
|
53
|
+
|
54
|
+
Table name where are stored the connection details of tenants:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
table_name: 'tenant'
|
58
|
+
```
|
59
|
+
|
60
|
+
Note: The columns of the table must contain connection details and
|
61
|
+
thad are according with the information needed to connect to a database,
|
62
|
+
including the name column to store the tenant name. Example columns
|
63
|
+
are showed below:
|
64
|
+
|
65
|
+
|Name |Type |
|
66
|
+
| ----------|:---------:|
|
67
|
+
| name | String |
|
68
|
+
| adapter | String |
|
69
|
+
| sockect | String |
|
70
|
+
| port | Integer |
|
71
|
+
| pool | Integer |
|
72
|
+
| host | Integer |
|
73
|
+
| username | Integer |
|
74
|
+
| password | Integer |
|
75
|
+
| database | Integer |
|
76
|
+
| url | String |
|
77
|
+
|
78
|
+
|
79
|
+
### Specifying a redis-server as a source of tenants
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
config.motel.tenants_source_configurations = {
|
83
|
+
source: :redis,
|
84
|
+
host: 127.0.0.1,
|
85
|
+
port: 6380,
|
86
|
+
password: 'redis_password'
|
87
|
+
}
|
88
|
+
```
|
89
|
+
To connect to Redis listening on a Unix socket, try use 'path'
|
90
|
+
option.
|
91
|
+
|
92
|
+
### Default source of tenants
|
93
|
+
|
94
|
+
Also you can use the gem without specify a source configuration.
|
95
|
+
|
96
|
+
If you want to assing dirently the tenants specificactions you can do it:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
config.motel.tenants_source_configurations = {
|
100
|
+
tenant: { 'foo' => { adapter: 'sqlite3', database: 'db/foo.sqlite3' }}
|
101
|
+
}
|
102
|
+
```
|
103
|
+
|
104
|
+
Assing tenants from database.yml file:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
config.motel.tenants_source_configurations = {
|
108
|
+
tenant: Rails.application.config.database_configuration
|
109
|
+
}
|
110
|
+
```
|
111
|
+
|
112
|
+
Note: The methods like `add_tenant`, `update_tenant` and
|
113
|
+
`delete_tenant` dosen't store permanently tenants.
|
114
|
+
|
115
|
+
### More configurations
|
116
|
+
|
117
|
+
You can assign a default tenant if the current tenant is null:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
config.motel.default_tenant = 'my_default_tenant'
|
121
|
+
```
|
122
|
+
|
123
|
+
Tenants switching is done via the subdomain of the url, you can
|
124
|
+
specify a criteria to identify the tenant providing a regex as a
|
125
|
+
string. Example, to get the tenant `foo` from the following url
|
126
|
+
`http://www.example.com/foo/index` you should write:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
config.motel.admission_criteria = '\/(\w*)\/'
|
130
|
+
```
|
131
|
+
|
132
|
+
To disable automatic switching between tenants by url you must
|
133
|
+
disable the middleware:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
config.motel.disable_middleware = true
|
137
|
+
```
|
138
|
+
|
139
|
+
Path of the html page to show if tenant doesn't exist. Default is
|
140
|
+
`public/404.html`.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
config.motel.nonexistent_tenant_page = 'new_path'
|
144
|
+
```
|
145
|
+
|
146
|
+
## Use without Rails
|
147
|
+
|
148
|
+
### Specifying the source of tenants
|
149
|
+
|
150
|
+
You can set the source of the tenants in the same way as with Rails, use the method `tenants_source_configurations` of `ActiveRecord::Base.motel`:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
ActiveRecord::Base.motel.tenants_source_configurations({
|
154
|
+
source: :database,
|
155
|
+
source_spec: { adapter: 'sqlite3', database: 'db/tenants.sqlite3' },
|
156
|
+
table_name: 'tenant'
|
157
|
+
})
|
158
|
+
```
|
159
|
+
|
160
|
+
# Available methods
|
161
|
+
|
162
|
+
Set a tenats source configurations
|
163
|
+
```ruby
|
164
|
+
ActiveRecord::Base.motel.tenants_source_configurations(config)
|
165
|
+
```
|
166
|
+
|
167
|
+
Set the admission criterio for the middleware
|
168
|
+
```ruby
|
169
|
+
ActiveRecord::Base.motel.admission_criterio
|
170
|
+
```
|
171
|
+
|
172
|
+
Set a default tenant
|
173
|
+
```ruby
|
174
|
+
ActiveRecord::Base.motel.default_tenant
|
175
|
+
```
|
176
|
+
|
177
|
+
Set the html page to show if tenant doesn't exist
|
178
|
+
```ruby
|
179
|
+
ActiveRecord::Base.motel.nonexistent_tenant_page
|
180
|
+
```
|
181
|
+
|
182
|
+
Set a current tenant
|
183
|
+
```ruby
|
184
|
+
ActiveRecord::Base.motel.current_tenant
|
185
|
+
```
|
186
|
+
|
187
|
+
Retrieve the connection details of all tenants
|
188
|
+
```ruby
|
189
|
+
ActiveRecord::Base.motel.tenants
|
190
|
+
```
|
191
|
+
|
192
|
+
Retrieve a tenant
|
193
|
+
```ruby
|
194
|
+
ActiveRecord::Base.motel.tenant(name)
|
195
|
+
```
|
196
|
+
|
197
|
+
Determine if a tenant exists
|
198
|
+
```ruby
|
199
|
+
ActiveRecord::Base.motel.tenant?(name)
|
200
|
+
```
|
201
|
+
|
202
|
+
Add tenant
|
203
|
+
```ruby
|
204
|
+
ActiveRecord::Base.motel.add_tenant(name, spec)
|
205
|
+
```
|
206
|
+
|
207
|
+
Update tenant
|
208
|
+
```ruby
|
209
|
+
ActiveRecord::Base.motel.update_tenant(name, spec)
|
210
|
+
```
|
211
|
+
|
212
|
+
Delete tenant
|
213
|
+
```ruby
|
214
|
+
ActiveRecord::Base.motel.delete_tenant(name)
|
215
|
+
```
|
216
|
+
|
217
|
+
Retrieve the names of the tenants of active connections
|
218
|
+
```ruby
|
219
|
+
ActiveRecord::Base.motel.active_tenants
|
220
|
+
```
|
221
|
+
|
222
|
+
Determine the tenant to use for the connection
|
223
|
+
```ruby
|
224
|
+
ActiveRecord::Base.motel.determines_tenant
|
225
|
+
```
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Motel
|
4
|
+
module ConnectionAdapters
|
5
|
+
|
6
|
+
class ConnectionHandler < ActiveRecord::ConnectionAdapters::ConnectionHandler
|
7
|
+
|
8
|
+
attr_accessor :tenants_source
|
9
|
+
|
10
|
+
def initialize(tenants_source = Sources::Default.new)
|
11
|
+
@owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
|
12
|
+
h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
|
13
|
+
end
|
14
|
+
@class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
|
15
|
+
h[k] = ThreadSafe::Cache.new
|
16
|
+
end
|
17
|
+
|
18
|
+
@tenants_source = tenants_source
|
19
|
+
end
|
20
|
+
|
21
|
+
def establish_connection(tenant_name, spec = nil)
|
22
|
+
spec ||= connection_especification(tenant_name)
|
23
|
+
@class_to_pool.clear
|
24
|
+
owner_to_pool[tenant_name] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec)
|
25
|
+
end
|
26
|
+
|
27
|
+
def retrieve_connection(tenant_name)
|
28
|
+
pool = retrieve_connection_pool(tenant_name)
|
29
|
+
|
30
|
+
unless pool.connection
|
31
|
+
establish_connection tenant_name
|
32
|
+
pool = retrieve_connection_pool(tenant_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
pool.connection
|
36
|
+
end
|
37
|
+
|
38
|
+
def retrieve_connection_pool(tenant_name)
|
39
|
+
class_to_pool[tenant_name] ||= begin
|
40
|
+
pool = pool_for(tenant_name)
|
41
|
+
|
42
|
+
class_to_pool[tenant_name] = pool
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_connection(tenant_name)
|
47
|
+
if pool = owner_to_pool.delete(tenant_name)
|
48
|
+
@class_to_pool.clear
|
49
|
+
pool.automatic_reconnect = false
|
50
|
+
pool.disconnect!
|
51
|
+
pool.spec.config
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def connected?(tenant_name)
|
56
|
+
conn = retrieve_connection_pool(tenant_name)
|
57
|
+
conn && conn.connected?
|
58
|
+
end
|
59
|
+
|
60
|
+
def pool_for(tenant_name)
|
61
|
+
owner_to_pool.fetch(tenant_name) {
|
62
|
+
establish_connection tenant_name
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def pool_from_any_process_for(tenant_name)
|
67
|
+
owner_to_pool = @owner_to_pool.values.find { |v| v[tenant_name] }
|
68
|
+
owner_to_pool && owner_to_pool[tenant_name]
|
69
|
+
end
|
70
|
+
|
71
|
+
def active_tenants
|
72
|
+
owner_to_pool.keys
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def connection_especification(tenant_name)
|
78
|
+
unless tenants_source.tenant?(tenant_name)
|
79
|
+
raise NonexistentTenantError, "Nonexistent #{tenant_name} tenant"
|
80
|
+
end
|
81
|
+
|
82
|
+
resolver = ConnectionSpecification::Resolver.new
|
83
|
+
spec = resolver.spec(tenants_source.tenant(tenant_name))
|
84
|
+
|
85
|
+
unless ActiveRecord::Base.respond_to?(spec.adapter_method)
|
86
|
+
raise ActiveRecord::AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
|
87
|
+
end
|
88
|
+
|
89
|
+
spec
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
4
|
+
|
5
|
+
module Motel
|
6
|
+
module ConnectionAdapters
|
7
|
+
module ConnectionSpecification
|
8
|
+
class Resolver
|
9
|
+
|
10
|
+
attr_accessor :configurations
|
11
|
+
|
12
|
+
def initialize(configurations = nil)
|
13
|
+
@configurations = configurations || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def spec(config)
|
17
|
+
spec = resolve(config).symbolize_keys
|
18
|
+
|
19
|
+
raise(ActiveRecord::AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
|
20
|
+
|
21
|
+
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
|
22
|
+
begin
|
23
|
+
require path_to_adapter
|
24
|
+
rescue Gem::LoadError => e
|
25
|
+
raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
|
26
|
+
rescue LoadError => e
|
27
|
+
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
|
28
|
+
end
|
29
|
+
|
30
|
+
adapter_method = "#{spec[:adapter]}_connection"
|
31
|
+
ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(spec, adapter_method)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def resolve(config)
|
37
|
+
case config
|
38
|
+
when nil
|
39
|
+
raise ActiveRecord::AdapterNotSpecified
|
40
|
+
when String, Symbol
|
41
|
+
resolve_string_connection config.to_s
|
42
|
+
when Hash
|
43
|
+
resolve_hash_connection config
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def resolve_hash_connection(spec)
|
48
|
+
if url = spec.delete("url")
|
49
|
+
connection_hash = resolve_string_connection(url)
|
50
|
+
spec.merge!(connection_hash)
|
51
|
+
end
|
52
|
+
spec
|
53
|
+
end
|
54
|
+
|
55
|
+
def resolve_string_connection(spec)
|
56
|
+
hash = configurations.fetch(spec) do |k|
|
57
|
+
connection_url_to_hash(k)
|
58
|
+
end
|
59
|
+
|
60
|
+
resolve_hash_connection hash
|
61
|
+
end
|
62
|
+
|
63
|
+
def connection_url_to_hash(url)
|
64
|
+
config = URI.parse url
|
65
|
+
adapter = config.scheme
|
66
|
+
adapter = "postgresql" if adapter == "postgres"
|
67
|
+
spec = { :adapter => adapter,
|
68
|
+
:username => config.user,
|
69
|
+
:password => config.password,
|
70
|
+
:port => config.port,
|
71
|
+
:database => config.path.sub(%r{^/},""),
|
72
|
+
:host => config.host }
|
73
|
+
|
74
|
+
spec.reject!{ |_,value| value.blank? }
|
75
|
+
|
76
|
+
uri_parser = URI::Parser.new
|
77
|
+
|
78
|
+
spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
|
79
|
+
|
80
|
+
if config.query
|
81
|
+
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
|
82
|
+
|
83
|
+
spec.merge!(options)
|
84
|
+
end
|
85
|
+
|
86
|
+
spec
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|