ros-apartment 3.2.0 → 3.3.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +93 -2
- data/.ruby-version +1 -1
- data/Appraisals +15 -0
- data/CLAUDE.md +210 -0
- data/README.md +1 -1
- data/Rakefile +6 -5
- data/docs/adapters.md +177 -0
- data/docs/architecture.md +274 -0
- data/docs/elevators.md +226 -0
- data/docs/images/log_example.png +0 -0
- data/lib/apartment/CLAUDE.md +300 -0
- data/lib/apartment/adapters/CLAUDE.md +314 -0
- data/lib/apartment/adapters/abstract_adapter.rb +24 -15
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
- data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
- data/lib/apartment/adapters/postgresql_adapter.rb +42 -19
- data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
- data/lib/apartment/console.rb +1 -1
- data/lib/apartment/custom_console.rb +7 -7
- data/lib/apartment/elevators/CLAUDE.md +292 -0
- data/lib/apartment/elevators/domain.rb +1 -1
- data/lib/apartment/elevators/generic.rb +1 -1
- data/lib/apartment/elevators/host_hash.rb +3 -3
- data/lib/apartment/elevators/subdomain.rb +9 -5
- data/lib/apartment/log_subscriber.rb +1 -1
- data/lib/apartment/migrator.rb +2 -2
- data/lib/apartment/model.rb +1 -1
- data/lib/apartment/railtie.rb +3 -3
- data/lib/apartment/tasks/enhancements.rb +1 -1
- data/lib/apartment/tasks/task_helper.rb +4 -4
- data/lib/apartment/tenant.rb +3 -3
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +15 -9
- data/lib/generators/apartment/install/install_generator.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +2 -2
- data/lib/tasks/apartment.rake +25 -25
- data/ros-apartment.gemspec +3 -3
- metadata +22 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22097d27933cc7eee7bf0c5e3ad5cf1b84d9243dcfc04be00d6db07a91c9857b
|
|
4
|
+
data.tar.gz: 28e20f76b32b532db8db681503753216ca2847c66ab80e902c200ea43a82b05d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f007c6b72251b371b02380628d53b5cf1c7b62e5620fb659b596e9dec1a62e7a2fc0bf0ad1f5253d99b852f31091ddcedf073ed5e64457c5430c58c79c8aa83
|
|
7
|
+
data.tar.gz: 53ccf42974cebbcc6f874f9a631fe366a3952ff36d993127173df34563848359f409a3beeaa37596896a75e8c85cccd63a5ce4a7ec70b7d3278bf7e3b8c7e394
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -7,7 +7,7 @@ AllCops:
|
|
|
7
7
|
- spec/dummy_engine/dummy_engine.gemspec
|
|
8
8
|
- spec/schemas/**/*.rb
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
plugins:
|
|
11
11
|
- rubocop-rails
|
|
12
12
|
- rubocop-performance
|
|
13
13
|
- rubocop-thread_safety
|
|
@@ -22,6 +22,26 @@ Layout/MultilineMethodCallIndentation:
|
|
|
22
22
|
EnforcedStyle: indented
|
|
23
23
|
|
|
24
24
|
Metrics/BlockLength:
|
|
25
|
+
Max: 30
|
|
26
|
+
Exclude:
|
|
27
|
+
- spec/**/*.rb
|
|
28
|
+
- lib/tasks/**/*.rake
|
|
29
|
+
- Rakefile
|
|
30
|
+
|
|
31
|
+
Metrics/MethodLength:
|
|
32
|
+
Max: 15
|
|
33
|
+
Exclude:
|
|
34
|
+
- spec/**/*.rb
|
|
35
|
+
- lib/apartment/tenant.rb
|
|
36
|
+
|
|
37
|
+
Metrics/AbcSize:
|
|
38
|
+
Max: 20
|
|
39
|
+
Exclude:
|
|
40
|
+
- spec/**/*.rb
|
|
41
|
+
- lib/apartment/adapters/postgresql_adapter.rb
|
|
42
|
+
|
|
43
|
+
Metrics/ClassLength:
|
|
44
|
+
Max: 155
|
|
25
45
|
Exclude:
|
|
26
46
|
- spec/**/*.rb
|
|
27
47
|
|
|
@@ -76,4 +96,75 @@ Style/CollectionMethods:
|
|
|
76
96
|
collect!: 'map!'
|
|
77
97
|
inject: 'reduce'
|
|
78
98
|
detect: 'detect'
|
|
79
|
-
find_all: 'select'
|
|
99
|
+
find_all: 'select'
|
|
100
|
+
|
|
101
|
+
# RSpec style preferences - disable for mature test suite
|
|
102
|
+
RSpec/NamedSubject:
|
|
103
|
+
Enabled: false
|
|
104
|
+
|
|
105
|
+
RSpec/MultipleExpectations:
|
|
106
|
+
Enabled: false
|
|
107
|
+
|
|
108
|
+
RSpec/MessageSpies:
|
|
109
|
+
Enabled: false
|
|
110
|
+
|
|
111
|
+
RSpec/NestedGroups:
|
|
112
|
+
Enabled: false
|
|
113
|
+
|
|
114
|
+
RSpec/ContextWording:
|
|
115
|
+
Enabled: false
|
|
116
|
+
|
|
117
|
+
RSpec/ExampleLength:
|
|
118
|
+
Enabled: false
|
|
119
|
+
|
|
120
|
+
RSpec/InstanceVariable:
|
|
121
|
+
Enabled: false
|
|
122
|
+
|
|
123
|
+
RSpec/SpecFilePathFormat:
|
|
124
|
+
Enabled: false
|
|
125
|
+
|
|
126
|
+
RSpec/DescribeClass:
|
|
127
|
+
Enabled: false
|
|
128
|
+
|
|
129
|
+
RSpec/IndexedLet:
|
|
130
|
+
Enabled: false
|
|
131
|
+
|
|
132
|
+
RSpec/AnyInstance:
|
|
133
|
+
Enabled: false
|
|
134
|
+
|
|
135
|
+
RSpec/BeforeAfterAll:
|
|
136
|
+
Enabled: false
|
|
137
|
+
|
|
138
|
+
RSpec/LeakyConstantDeclaration:
|
|
139
|
+
Enabled: false
|
|
140
|
+
|
|
141
|
+
RSpec/VerifiedDoubles:
|
|
142
|
+
Enabled: false
|
|
143
|
+
|
|
144
|
+
RSpec/NoExpectationExample:
|
|
145
|
+
Enabled: false
|
|
146
|
+
|
|
147
|
+
# ThreadSafety - intentional design for configuration
|
|
148
|
+
ThreadSafety/ClassInstanceVariable:
|
|
149
|
+
Exclude:
|
|
150
|
+
- lib/apartment.rb
|
|
151
|
+
- lib/apartment/model.rb
|
|
152
|
+
- lib/apartment/elevators/*.rb
|
|
153
|
+
- spec/support/config.rb
|
|
154
|
+
|
|
155
|
+
ThreadSafety/ClassAndModuleAttributes:
|
|
156
|
+
Exclude:
|
|
157
|
+
- lib/apartment.rb
|
|
158
|
+
- lib/apartment/active_record/postgresql_adapter.rb
|
|
159
|
+
|
|
160
|
+
ThreadSafety/DirChdir:
|
|
161
|
+
Exclude:
|
|
162
|
+
- ros-apartment.gemspec
|
|
163
|
+
|
|
164
|
+
ThreadSafety/NewThread:
|
|
165
|
+
Exclude:
|
|
166
|
+
- spec/tenant_spec.rb
|
|
167
|
+
|
|
168
|
+
# Rake cops
|
|
169
|
+
Rake/DuplicateTask:
|
|
170
|
+
Enabled: false
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.3.
|
|
1
|
+
3.3.10
|
data/Appraisals
CHANGED
|
@@ -128,3 +128,18 @@ appraise 'rails-8-0-sqlite3' do
|
|
|
128
128
|
gem 'rails', '~> 8.0.0'
|
|
129
129
|
gem 'sqlite3', '~> 2.1'
|
|
130
130
|
end
|
|
131
|
+
|
|
132
|
+
appraise 'rails-8-1-postgresql' do
|
|
133
|
+
gem 'rails', '~> 8.1.0'
|
|
134
|
+
gem 'pg', '~> 1.6.0'
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
appraise 'rails-8-1-mysql' do
|
|
138
|
+
gem 'rails', '~> 8.1.0'
|
|
139
|
+
gem 'mysql2', '~> 0.5'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
appraise 'rails-8-1-sqlite3' do
|
|
143
|
+
gem 'rails', '~> 8.1.0'
|
|
144
|
+
gem 'sqlite3', '~> 2.8'
|
|
145
|
+
end
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# CLAUDE.md - Apartment v3 Understanding Guide
|
|
2
|
+
|
|
3
|
+
**Version**: 3.x (Current Development Branch)
|
|
4
|
+
**Maintained by**: CampusESP
|
|
5
|
+
**Gem Name**: `ros-apartment`
|
|
6
|
+
|
|
7
|
+
## What This Documentation Covers
|
|
8
|
+
|
|
9
|
+
This branch contains v3 (current stable release). A v4 refactor with different architecture exists on `man/spec-restart` branch.
|
|
10
|
+
|
|
11
|
+
**Goal**: Understand v3 deeply enough to maintain it and plan v4 migration.
|
|
12
|
+
|
|
13
|
+
## Where to Start
|
|
14
|
+
|
|
15
|
+
1. **README.md** - Installation, basic usage, configuration options
|
|
16
|
+
2. **docs/architecture.md** - Core design decisions and WHY they were made
|
|
17
|
+
3. **docs/adapters.md** - Database strategy trade-offs
|
|
18
|
+
4. **docs/elevators.md** - Middleware design rationale
|
|
19
|
+
5. **lib/apartment/CLAUDE.md** - Implementation file guide
|
|
20
|
+
6. **spec/CLAUDE.md** - Test organization and patterns
|
|
21
|
+
|
|
22
|
+
## Core Concepts
|
|
23
|
+
|
|
24
|
+
### Multi-Tenancy via Database Isolation
|
|
25
|
+
|
|
26
|
+
**Problem**: Single application needs to serve multiple customers with data completely separated.
|
|
27
|
+
|
|
28
|
+
**v3 Solution**: Thread-local tenant switching. Each request/thread tracks which tenant it's serving.
|
|
29
|
+
|
|
30
|
+
**Key limitation**: Not fiber-safe (fibers share thread-local storage).
|
|
31
|
+
|
|
32
|
+
### Two Main Strategies
|
|
33
|
+
|
|
34
|
+
**PostgreSQL (schemas)**: Multiple namespaces in single database. Fast, scales to 100+ tenants.
|
|
35
|
+
|
|
36
|
+
**MySQL (databases)**: Separate database per tenant. Complete isolation, slower switching.
|
|
37
|
+
|
|
38
|
+
**See**: `docs/adapters.md` for trade-offs.
|
|
39
|
+
|
|
40
|
+
### Automatic Tenant Detection
|
|
41
|
+
|
|
42
|
+
**Middleware ("Elevators")**: Rack middleware extracts tenant from request (subdomain, domain, header).
|
|
43
|
+
|
|
44
|
+
**Critical**: Must position before session middleware to avoid data leakage.
|
|
45
|
+
|
|
46
|
+
**See**: `docs/elevators.md` for design decisions.
|
|
47
|
+
|
|
48
|
+
## Key Architecture Decisions
|
|
49
|
+
|
|
50
|
+
### 1. Thread-Local Adapter Storage
|
|
51
|
+
|
|
52
|
+
**Why**: Concurrent requests need isolated tenant contexts without global locks.
|
|
53
|
+
|
|
54
|
+
**Implementation**: `Thread.current[:apartment_adapter]`
|
|
55
|
+
|
|
56
|
+
**Trade-off**: Not fiber-safe, but works for 99% of Rails deployments.
|
|
57
|
+
|
|
58
|
+
**See**: `Apartment::Tenant.adapter` method in `tenant.rb`, `docs/architecture.md`
|
|
59
|
+
|
|
60
|
+
### 2. Block-Based Tenant Switching
|
|
61
|
+
|
|
62
|
+
**Why**: Automatic cleanup even on exceptions prevents tenant context leakage.
|
|
63
|
+
|
|
64
|
+
**Pattern**: `Apartment::Tenant.switch(tenant) { ... }` with ensure block
|
|
65
|
+
|
|
66
|
+
**Alternative rejected**: Manual switch/reset - too error-prone.
|
|
67
|
+
|
|
68
|
+
**See**: `AbstractAdapter#switch` method in `adapters/abstract_adapter.rb`
|
|
69
|
+
|
|
70
|
+
### 3. Excluded Models
|
|
71
|
+
|
|
72
|
+
**Why**: Some models (User, Company) exist globally across all tenants.
|
|
73
|
+
|
|
74
|
+
**Implementation**: Separate connections that bypass tenant switching.
|
|
75
|
+
|
|
76
|
+
**Limitation**: Can't use `has_and_belongs_to_many` - must use `has_many :through`.
|
|
77
|
+
|
|
78
|
+
**See**: `AbstractAdapter#process_excluded_models` method in `adapters/abstract_adapter.rb`
|
|
79
|
+
|
|
80
|
+
### 4. Adapter Pattern
|
|
81
|
+
|
|
82
|
+
**Why**: PostgreSQL uses schemas, MySQL uses databases - fundamentally different.
|
|
83
|
+
|
|
84
|
+
**Implementation**: Abstract base class with database-specific subclasses.
|
|
85
|
+
|
|
86
|
+
**Benefit**: Unified API hides database differences.
|
|
87
|
+
|
|
88
|
+
**See**: `lib/apartment/adapters/`, `docs/adapters.md`
|
|
89
|
+
|
|
90
|
+
### 5. Callback System
|
|
91
|
+
|
|
92
|
+
**Why**: Users need logging/notification hooks without modifying gem code.
|
|
93
|
+
|
|
94
|
+
**Implementation**: ActiveSupport::Callbacks on `:create` and `:switch`.
|
|
95
|
+
|
|
96
|
+
**See**: Callback definitions in `AbstractAdapter` class in `adapters/abstract_adapter.rb`
|
|
97
|
+
|
|
98
|
+
## File Organization
|
|
99
|
+
|
|
100
|
+
**Core logic**: `lib/apartment.rb` (configuration), `lib/apartment/tenant.rb` (public API)
|
|
101
|
+
|
|
102
|
+
**Adapters**: `lib/apartment/adapters/*.rb` - Database-specific implementations
|
|
103
|
+
|
|
104
|
+
**Elevators**: `lib/apartment/elevators/*.rb` - Rack middleware for auto-switching
|
|
105
|
+
|
|
106
|
+
**Tests**: `spec/` - Adapter tests, elevator tests, integration tests
|
|
107
|
+
|
|
108
|
+
**See folder CLAUDE.md files for details on each directory.**
|
|
109
|
+
|
|
110
|
+
## Configuration Philosophy
|
|
111
|
+
|
|
112
|
+
**Dynamic tenant discovery**: `tenant_names` can be callable (proc/lambda) that queries database. Why? Tenants change at runtime.
|
|
113
|
+
|
|
114
|
+
**Fail-safe boot**: Rescue database errors during config loading. Why? App should start even if tenant table doesn't exist yet (pending migrations).
|
|
115
|
+
|
|
116
|
+
**Environment isolation**: Optional `prepend_environment`/`append_environment` to prevent cross-environment tenant name collisions.
|
|
117
|
+
|
|
118
|
+
**See**: `Apartment.extract_tenant_config` method in `lib/apartment.rb`
|
|
119
|
+
|
|
120
|
+
## Common Pitfalls
|
|
121
|
+
|
|
122
|
+
**Elevator positioning**: Must be before session/auth middleware. Otherwise session data leaks across tenants.
|
|
123
|
+
|
|
124
|
+
**Not using blocks**: `switch!` without block requires manual cleanup. Easy to forget. Always prefer `switch` with block.
|
|
125
|
+
|
|
126
|
+
**HABTM with excluded models**: Doesn't work. Must use `has_many :through` instead.
|
|
127
|
+
|
|
128
|
+
**Assuming fiber safety**: v3 uses thread-local storage. Not safe for fiber-based async frameworks.
|
|
129
|
+
|
|
130
|
+
**See**: `docs/architecture.md` for detailed analysis
|
|
131
|
+
|
|
132
|
+
## Performance Characteristics
|
|
133
|
+
|
|
134
|
+
**PostgreSQL schemas**:
|
|
135
|
+
- Switch: <1ms
|
|
136
|
+
- Scalability: 100+ tenants
|
|
137
|
+
- Memory: Constant
|
|
138
|
+
|
|
139
|
+
**MySQL databases**:
|
|
140
|
+
- Switch: 10-50ms
|
|
141
|
+
- Scalability: 10-50 tenants
|
|
142
|
+
- Memory: Linear with active tenants
|
|
143
|
+
|
|
144
|
+
**See**: `docs/adapters.md` for benchmarks and trade-offs
|
|
145
|
+
|
|
146
|
+
## Testing the Gem
|
|
147
|
+
|
|
148
|
+
**Spec organization**: `spec/adapters/` for database tests, `spec/unit/elevators/` for middleware tests
|
|
149
|
+
|
|
150
|
+
**Database selection**: `DB=postgresql rspec` or `DB=mysql` or `DB=sqlite3`
|
|
151
|
+
|
|
152
|
+
**Key test pattern**: Create test tenant, switch to it, verify isolation, cleanup
|
|
153
|
+
|
|
154
|
+
**See**: `spec/CLAUDE.md` for testing patterns
|
|
155
|
+
|
|
156
|
+
## Debugging Techniques
|
|
157
|
+
|
|
158
|
+
**Check current tenant**: `Apartment::Tenant.current`
|
|
159
|
+
|
|
160
|
+
**Inspect adapter**: `Apartment::Tenant.adapter.class`
|
|
161
|
+
|
|
162
|
+
**List tenants**: `Apartment.tenant_names`
|
|
163
|
+
|
|
164
|
+
**Enable logging**: `config.active_record_log = true`
|
|
165
|
+
|
|
166
|
+
**PostgreSQL search path**: `SHOW search_path` in SQL console
|
|
167
|
+
|
|
168
|
+
**See**: Inline code comments for context-specific debugging
|
|
169
|
+
|
|
170
|
+
## Migration to v4
|
|
171
|
+
|
|
172
|
+
**v4 branch**: `man/spec-restart`
|
|
173
|
+
|
|
174
|
+
**Major changes**: Connection pool per tenant (vs thread-local switching), fiber-safe via CurrentAttributes, immutable connection descriptors
|
|
175
|
+
|
|
176
|
+
**Why v4**: Better performance (no switching overhead), true fiber safety, simpler mental model
|
|
177
|
+
|
|
178
|
+
**Migration strategy**: Understand v3 architecture first (this branch), then contrast with v4 approach
|
|
179
|
+
|
|
180
|
+
## Design Principles
|
|
181
|
+
|
|
182
|
+
**Open for extension**: Users can create custom adapters and elevators without modifying gem.
|
|
183
|
+
|
|
184
|
+
**Closed for modification**: Core logic shouldn't need changes for new use cases.
|
|
185
|
+
|
|
186
|
+
**Fail fast**: Configuration errors raise at boot. Tenant not found raises at runtime.
|
|
187
|
+
|
|
188
|
+
**Graceful degradation**: If rollback fails, fall back to default tenant rather than crash.
|
|
189
|
+
|
|
190
|
+
**See**: `docs/architecture.md` for rationale
|
|
191
|
+
|
|
192
|
+
## Getting Help
|
|
193
|
+
|
|
194
|
+
**Issues**: https://github.com/rails-on-services/apartment/issues
|
|
195
|
+
|
|
196
|
+
**Discussions**: https://github.com/rails-on-services/apartment/discussions
|
|
197
|
+
|
|
198
|
+
**Code**: Read the actual implementation files - they're well-commented
|
|
199
|
+
|
|
200
|
+
## Documentation Philosophy
|
|
201
|
+
|
|
202
|
+
**This documentation focuses on WHY, not HOW**:
|
|
203
|
+
- Design decisions and trade-offs
|
|
204
|
+
- Architecture rationale
|
|
205
|
+
- Pitfalls and constraints
|
|
206
|
+
- References to actual source files
|
|
207
|
+
|
|
208
|
+
**For HOW (implementation details)**: Read the well-commented source code in `lib/`.
|
|
209
|
+
|
|
210
|
+
**For WHAT (API reference)**: See README.md and RDoc comments.
|
data/README.md
CHANGED
|
@@ -348,7 +348,7 @@ Please note that our custom logger inherits from `ActiveRecord::LogSubscriber` s
|
|
|
348
348
|
|
|
349
349
|
**Example log output:**
|
|
350
350
|
|
|
351
|
-
<img src="
|
|
351
|
+
<img src="docs/images/log_example.png">
|
|
352
352
|
|
|
353
353
|
```ruby
|
|
354
354
|
Apartment.configure do |config|
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
begin
|
|
4
|
-
require
|
|
4
|
+
require('bundler')
|
|
5
5
|
rescue StandardError
|
|
6
6
|
'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
7
7
|
end
|
|
@@ -26,6 +26,7 @@ namespace :spec do
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
desc 'Start an interactive console with Apartment loaded'
|
|
29
30
|
task :console do
|
|
30
31
|
require 'pry'
|
|
31
32
|
require 'apartment'
|
|
@@ -39,15 +40,15 @@ namespace :db do
|
|
|
39
40
|
namespace :test do
|
|
40
41
|
case ENV.fetch('DATABASE_ENGINE', nil)
|
|
41
42
|
when 'postgresql'
|
|
42
|
-
task
|
|
43
|
+
task(prepare: %w[postgres:drop_db postgres:build_db])
|
|
43
44
|
when 'mysql'
|
|
44
|
-
task
|
|
45
|
+
task(prepare: %w[mysql:drop_db mysql:build_db])
|
|
45
46
|
when 'sqlite'
|
|
46
|
-
task
|
|
47
|
+
task(:prepare) do
|
|
47
48
|
puts 'No need to prepare sqlite3 database'
|
|
48
49
|
end
|
|
49
50
|
else
|
|
50
|
-
task
|
|
51
|
+
task(:prepare) do
|
|
51
52
|
puts 'No database engine specified, skipping db:test:prepare'
|
|
52
53
|
end
|
|
53
54
|
end
|
data/docs/adapters.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Apartment Adapters - Design & Architecture
|
|
2
|
+
|
|
3
|
+
**Key files**: `lib/apartment/adapters/*.rb`
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Adapters translate abstract tenant operations into database-specific implementations. Each database has fundamentally different isolation mechanisms, requiring separate strategies.
|
|
8
|
+
|
|
9
|
+
## Design Decision: Why Adapter Pattern?
|
|
10
|
+
|
|
11
|
+
**Problem**: PostgreSQL uses schemas, MySQL uses databases, SQLite uses files. A unified API across these different approaches requires abstraction.
|
|
12
|
+
|
|
13
|
+
**Solution**: Adapter pattern with shared base class defining lifecycle, database-specific subclasses implementing mechanics.
|
|
14
|
+
|
|
15
|
+
**Trade-off**: Adds complexity but enables multi-database support without polluting core logic.
|
|
16
|
+
|
|
17
|
+
## Adapter Hierarchy
|
|
18
|
+
|
|
19
|
+
See `lib/apartment/adapters/` for implementations:
|
|
20
|
+
- `abstract_adapter.rb` - Shared lifecycle, callbacks, error handling
|
|
21
|
+
- `postgresql_adapter.rb` - Schema-based isolation (3 variants)
|
|
22
|
+
- `mysql2_adapter.rb` - Database-per-tenant
|
|
23
|
+
- `sqlite3_adapter.rb` - File-per-tenant
|
|
24
|
+
- JDBC variants for JRuby
|
|
25
|
+
|
|
26
|
+
## AbstractAdapter - Design Rationale
|
|
27
|
+
|
|
28
|
+
**File**: `lib/apartment/adapters/abstract_adapter.rb`
|
|
29
|
+
|
|
30
|
+
### Why Callbacks?
|
|
31
|
+
|
|
32
|
+
Provides extension points for logging, notifications, analytics without modifying core adapter code. Users can hook into `:create` and `:switch` events.
|
|
33
|
+
|
|
34
|
+
### Why Ensure Blocks in switch()?
|
|
35
|
+
|
|
36
|
+
**Critical decision**: Always rollback to previous tenant, even if block raises. Prevents tenant context leakage across requests/jobs. If rollback fails, fall back to default tenant as last resort.
|
|
37
|
+
|
|
38
|
+
**Alternative considered**: Let exceptions propagate without cleanup. Rejected because it leaves connections in wrong tenant state.
|
|
39
|
+
|
|
40
|
+
### Why Query Cache Management?
|
|
41
|
+
|
|
42
|
+
Rails disables query cache during connection establishment. Must explicitly preserve and restore state across tenant switches to maintain performance.
|
|
43
|
+
|
|
44
|
+
### Why Separate Connection Handler?
|
|
45
|
+
|
|
46
|
+
`SeparateDbConnectionHandler` prevents admin operations (CREATE/DROP DATABASE) from polluting the main application connection pool. Multi-server setups especially need this isolation.
|
|
47
|
+
|
|
48
|
+
## PostgreSQL Adapters - Three Strategies
|
|
49
|
+
|
|
50
|
+
**Files**: `postgresql_adapter.rb` (3 classes in one file)
|
|
51
|
+
|
|
52
|
+
### 1. PostgresqlAdapter (Database-per-tenant)
|
|
53
|
+
|
|
54
|
+
Rarely used. Most deployments use schemas instead.
|
|
55
|
+
|
|
56
|
+
### 2. PostgresqlSchemaAdapter (Schema-based - Primary)
|
|
57
|
+
|
|
58
|
+
**Why schemas?**: Single database, multiple namespaces. Fast switching via `SET search_path`. Scales to hundreds of tenants without connection overhead.
|
|
59
|
+
|
|
60
|
+
**Key design decisions**:
|
|
61
|
+
- **Search path ordering**: Tenant schema first, then persistent schemas, then public. Tables resolve in order.
|
|
62
|
+
- **Persistent schemas**: Shared extensions (PostGIS, uuid-ossp) remain accessible across all tenants.
|
|
63
|
+
- **Excluded model handling**: Explicitly qualify table names with default schema to prevent tenant-based queries.
|
|
64
|
+
|
|
65
|
+
**Trade-off**: Less isolation than separate databases, but massively better performance and scalability.
|
|
66
|
+
|
|
67
|
+
### 3. PostgresqlSchemaFromSqlAdapter (pg_dump-based)
|
|
68
|
+
|
|
69
|
+
**Why pg_dump instead of schema.rb?**:
|
|
70
|
+
- Handles PostgreSQL-specific features (extensions, custom types, constraints) that Rails schema dumper misses
|
|
71
|
+
- Required for PostGIS spatial types
|
|
72
|
+
- Necessary for complex production schemas
|
|
73
|
+
|
|
74
|
+
**Why patch search_path in dump?**: pg_dump outputs assume specific search_path. Must rewrite SQL to target new tenant schema instead of source schema.
|
|
75
|
+
|
|
76
|
+
**Why environment variable handling?**: pg_dump shell command reads PGHOST/PGUSER/etc from ENV. Must temporarily set, execute, then restore to avoid polluting global state.
|
|
77
|
+
|
|
78
|
+
**Alternative considered**: Use Rails schema.rb. Rejected because it loses PostgreSQL-specific features.
|
|
79
|
+
|
|
80
|
+
## MySQL Adapters - Database Isolation
|
|
81
|
+
|
|
82
|
+
**Files**: `mysql2_adapter.rb`, `trilogy_adapter.rb`
|
|
83
|
+
|
|
84
|
+
### Why Separate Databases?
|
|
85
|
+
|
|
86
|
+
MySQL doesn't have PostgreSQL's robust schema support. Database-level isolation is the natural fit.
|
|
87
|
+
|
|
88
|
+
**Implications**:
|
|
89
|
+
- Each switch establishes new connection to different database
|
|
90
|
+
- Connection pool per tenant (memory overhead)
|
|
91
|
+
- Practical limit of 10-50 concurrent tenants before connection exhaustion
|
|
92
|
+
|
|
93
|
+
### Why Trilogy Adapter?
|
|
94
|
+
|
|
95
|
+
Modern MySQL driver. Identical implementation to Mysql2Adapter, just different gem.
|
|
96
|
+
|
|
97
|
+
### Multi-Server Support
|
|
98
|
+
|
|
99
|
+
Hash-based tenant config allows different tenants on different MySQL servers. Enables horizontal scaling and geographic distribution.
|
|
100
|
+
|
|
101
|
+
## SQLite Adapter - File-Based
|
|
102
|
+
|
|
103
|
+
**File**: `sqlite3_adapter.rb`
|
|
104
|
+
|
|
105
|
+
### Why File-Per-Tenant?
|
|
106
|
+
|
|
107
|
+
SQLite is single-file database. Natural isolation is separate files.
|
|
108
|
+
|
|
109
|
+
**Use case**: Testing, development, single-user apps. **Not** production multi-tenant.
|
|
110
|
+
|
|
111
|
+
## Performance Characteristics
|
|
112
|
+
|
|
113
|
+
**PostgreSQL schemas**:
|
|
114
|
+
- Switch latency: <1ms (SQL command)
|
|
115
|
+
- Scalability: 100+ tenants easily
|
|
116
|
+
- Memory: Constant (~50MB)
|
|
117
|
+
|
|
118
|
+
**MySQL databases**:
|
|
119
|
+
- Switch latency: 10-50ms (connection establishment)
|
|
120
|
+
- Scalability: 10-50 tenants
|
|
121
|
+
- Memory: ~20MB per active tenant
|
|
122
|
+
|
|
123
|
+
**SQLite files**:
|
|
124
|
+
- Switch latency: 5-20ms (file I/O)
|
|
125
|
+
- Scalability: Not recommended for concurrent users
|
|
126
|
+
- Memory: ~5MB per database
|
|
127
|
+
|
|
128
|
+
## Adapter Selection Matrix
|
|
129
|
+
|
|
130
|
+
| Database | Strategy | Speed | Scalability | Isolation | Best For |
|
|
131
|
+
|------------|--------------|-----------|-------------|-----------|-----------------------|
|
|
132
|
+
| PostgreSQL | Schemas | Very Fast | Excellent | Good | 100+ tenants |
|
|
133
|
+
| MySQL | Databases | Moderate | Good | Excellent | Complete isolation |
|
|
134
|
+
| SQLite | Files | Moderate | Poor | Excellent | Testing only |
|
|
135
|
+
|
|
136
|
+
## Extension Points
|
|
137
|
+
|
|
138
|
+
### Creating Custom Adapters
|
|
139
|
+
|
|
140
|
+
**Why you might need this**: Supporting databases not yet implemented (Oracle, SQL Server, CockroachDB).
|
|
141
|
+
|
|
142
|
+
**What to implement**:
|
|
143
|
+
1. Subclass `AbstractAdapter`
|
|
144
|
+
2. Define required methods: `create_tenant`, `connect_to_new`, `drop_command`, `current`
|
|
145
|
+
3. Register factory method in `lib/apartment/tenant.rb`
|
|
146
|
+
|
|
147
|
+
**See**: Existing adapters for patterns. PostgreSQL is most complex, SQLite is simplest.
|
|
148
|
+
|
|
149
|
+
## Common Pitfalls & Design Constraints
|
|
150
|
+
|
|
151
|
+
### Why Transaction Handling in create_tenant?
|
|
152
|
+
|
|
153
|
+
RSpec tests run in transactions. Must detect open transactions and avoid nested BEGIN/COMMIT to prevent errors.
|
|
154
|
+
|
|
155
|
+
### Why Separate rescue_from per Adapter?
|
|
156
|
+
|
|
157
|
+
Different databases raise different exceptions. PostgreSQL raises `PG::Error`, MySQL raises different errors. Each adapter specifies what to rescue.
|
|
158
|
+
|
|
159
|
+
### Why environmentify()?
|
|
160
|
+
|
|
161
|
+
Prevents tenant name collisions across Rails environments. `development_acme` vs `production_acme`. Optional but recommended for shared infrastructure.
|
|
162
|
+
|
|
163
|
+
## Thread Safety
|
|
164
|
+
|
|
165
|
+
**Critical**: Adapters stored in `Thread.current[:apartment_adapter]`. Each thread gets isolated adapter instance.
|
|
166
|
+
|
|
167
|
+
**Implication**: Safe for multi-threaded servers (Puma), background jobs (Sidekiq).
|
|
168
|
+
|
|
169
|
+
**Limitation**: Not fiber-safe. v4 refactor addresses this.
|
|
170
|
+
|
|
171
|
+
## References
|
|
172
|
+
|
|
173
|
+
- AbstractAdapter implementation: `lib/apartment/adapters/abstract_adapter.rb`
|
|
174
|
+
- PostgreSQL variants: `lib/apartment/adapters/postgresql_adapter.rb`
|
|
175
|
+
- MySQL variants: `lib/apartment/adapters/mysql2_adapter.rb`, `trilogy_adapter.rb`
|
|
176
|
+
- SQLite: `lib/apartment/adapters/sqlite3_adapter.rb`
|
|
177
|
+
- PostgreSQL documentation: https://www.postgresql.org/docs/current/ddl-schemas.html
|