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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Apartment v3 Architecture - Design Decisions
|
|
2
|
+
|
|
3
|
+
**Core files**: `lib/apartment.rb`, `lib/apartment/tenant.rb`
|
|
4
|
+
|
|
5
|
+
## Architectural Philosophy
|
|
6
|
+
|
|
7
|
+
Apartment v3 uses **thread-local state** for tenant tracking. Each thread maintains its own adapter instance, enabling concurrent request handling without cross-contamination.
|
|
8
|
+
|
|
9
|
+
**Critical design constraint**: This architecture is **not fiber-safe**. The v4 refactor addresses this limitation.
|
|
10
|
+
|
|
11
|
+
## Core Design Patterns
|
|
12
|
+
|
|
13
|
+
### 1. Adapter Pattern
|
|
14
|
+
|
|
15
|
+
**Why**: Different databases require fundamentally different isolation strategies (PostgreSQL schemas vs MySQL databases vs SQLite files).
|
|
16
|
+
|
|
17
|
+
**Implementation**: `AbstractAdapter` defines lifecycle, database-specific subclasses implement mechanics.
|
|
18
|
+
|
|
19
|
+
**Trade-off**: Adds abstraction layer but enables multi-database support.
|
|
20
|
+
|
|
21
|
+
**See**: `lib/apartment/adapters/`
|
|
22
|
+
|
|
23
|
+
### 2. Delegation Pattern
|
|
24
|
+
|
|
25
|
+
**Why**: Simplify public API while maintaining internal flexibility.
|
|
26
|
+
|
|
27
|
+
**Implementation**: `Apartment::Tenant` delegates all operations to the thread-local adapter instance.
|
|
28
|
+
|
|
29
|
+
**Benefit**: Swap adapter implementations without changing user-facing code.
|
|
30
|
+
|
|
31
|
+
**See**: `lib/apartment/tenant.rb` - uses `def_delegators`
|
|
32
|
+
|
|
33
|
+
### 3. Thread-Local Storage Pattern
|
|
34
|
+
|
|
35
|
+
**Why**: Concurrent requests need isolated tenant contexts.
|
|
36
|
+
|
|
37
|
+
**Implementation**: Adapter stored in `Thread.current[:apartment_adapter]`.
|
|
38
|
+
|
|
39
|
+
**Safe for**:
|
|
40
|
+
- Multi-threaded web servers (Puma, Falcon)
|
|
41
|
+
- Background job processors (Sidekiq with threading)
|
|
42
|
+
- Concurrent requests to different tenants
|
|
43
|
+
|
|
44
|
+
**Unsafe for**:
|
|
45
|
+
- Fiber-based async frameworks (fibers share thread storage)
|
|
46
|
+
- Manual thread management with shared state
|
|
47
|
+
|
|
48
|
+
**Alternative considered**: Global state with mutex locking. Rejected due to contention and complexity.
|
|
49
|
+
|
|
50
|
+
**See**: `Apartment::Tenant.adapter` method in `tenant.rb`
|
|
51
|
+
|
|
52
|
+
### 4. Callback Pattern
|
|
53
|
+
|
|
54
|
+
**Why**: Users need extension points without modifying gem code.
|
|
55
|
+
|
|
56
|
+
**Implementation**: ActiveSupport::Callbacks on `:create` and `:switch` events.
|
|
57
|
+
|
|
58
|
+
**Use cases**: Logging, notifications, analytics, APM integration.
|
|
59
|
+
|
|
60
|
+
**See**: Callback definitions in `AbstractAdapter` class
|
|
61
|
+
|
|
62
|
+
### 5. Strategy Pattern (Elevators)
|
|
63
|
+
|
|
64
|
+
**Why**: Different applications need different tenant resolution mechanisms (subdomain, domain, header, session).
|
|
65
|
+
|
|
66
|
+
**Implementation**: Pluggable Rack middleware with customizable `parse_tenant_name`.
|
|
67
|
+
|
|
68
|
+
**Benefit**: Easy to add custom strategies without changing core.
|
|
69
|
+
|
|
70
|
+
**See**: `lib/apartment/elevators/`
|
|
71
|
+
|
|
72
|
+
## Component Interaction
|
|
73
|
+
|
|
74
|
+
### Request Processing Flow
|
|
75
|
+
|
|
76
|
+
**Path**: Rack request → Elevator → Adapter → Database
|
|
77
|
+
|
|
78
|
+
**Key decision points**:
|
|
79
|
+
1. **Elevator positioning**: Must be before session/auth middleware. Why? Tenant context must be established before session data loads, otherwise wrong tenant's sessions leak.
|
|
80
|
+
|
|
81
|
+
2. **Automatic cleanup**: `ensure` blocks in `switch()` guarantee tenant rollback even on exceptions. Why? Prevents connection staying in wrong tenant after errors.
|
|
82
|
+
|
|
83
|
+
3. **Query cache management**: Explicitly preserve across switches. Why? Rails disables during connection establishment; must manually restore to maintain performance.
|
|
84
|
+
|
|
85
|
+
**See**: `lib/apartment/elevators/generic.rb` - base middleware pattern
|
|
86
|
+
|
|
87
|
+
### Tenant Creation Flow
|
|
88
|
+
|
|
89
|
+
**Path**: User code → Adapter → Database → Schema import → Seeding
|
|
90
|
+
|
|
91
|
+
**Key decisions**:
|
|
92
|
+
1. **Callback execution**: Wraps entire creation in callbacks. Why? Logging and notifications must capture the complete operation.
|
|
93
|
+
|
|
94
|
+
2. **Switch during creation**: Import and seed run in tenant context. Why? Schema loading must target new tenant, not default.
|
|
95
|
+
|
|
96
|
+
3. **Transaction handling**: Detect existing transactions (RSpec). Why? Avoid nested transactions that PostgreSQL rejects.
|
|
97
|
+
|
|
98
|
+
**See**: `AbstractAdapter#create` method
|
|
99
|
+
|
|
100
|
+
### Configuration Resolution
|
|
101
|
+
|
|
102
|
+
**Why dynamic tenant lists?**: Tenants change at runtime (new signups, deletions). Static lists become stale.
|
|
103
|
+
|
|
104
|
+
**Implementation**: `tenant_names` can be callable (proc/lambda) that queries database.
|
|
105
|
+
|
|
106
|
+
**Critical handling**: Rescue `ActiveRecord::StatementInvalid` during boot. Why? Table might not exist yet (migrations pending). Return empty array to allow app to start.
|
|
107
|
+
|
|
108
|
+
**See**: `Apartment.extract_tenant_config` method
|
|
109
|
+
|
|
110
|
+
## Data Flow Differences by Database
|
|
111
|
+
|
|
112
|
+
### PostgreSQL Schema Strategy
|
|
113
|
+
|
|
114
|
+
**Mechanism**: Single connection pool, `SET search_path` per query.
|
|
115
|
+
|
|
116
|
+
**Why this works**: PostgreSQL schemas are namespaces. Queries resolve to first matching table in search path.
|
|
117
|
+
|
|
118
|
+
**Memory efficiency**: Connection pool shared across all tenants. Only schema metadata grows with tenant count.
|
|
119
|
+
|
|
120
|
+
**Performance**: Sub-millisecond switching (simple SQL command).
|
|
121
|
+
|
|
122
|
+
**Limitation**: All tenants in same database. Backup/restore is database-wide.
|
|
123
|
+
|
|
124
|
+
### MySQL Database Strategy
|
|
125
|
+
|
|
126
|
+
**Mechanism**: Separate connection pool per tenant.
|
|
127
|
+
|
|
128
|
+
**Why different from PostgreSQL**: MySQL lacks robust schema support. Database is natural isolation unit.
|
|
129
|
+
|
|
130
|
+
**Memory cost**: Each active tenant requires connection pool (~20MB).
|
|
131
|
+
|
|
132
|
+
**Performance**: Slower switching (connection establishment overhead).
|
|
133
|
+
|
|
134
|
+
**Benefit**: Complete isolation. Can backup/restore individual tenants.
|
|
135
|
+
|
|
136
|
+
### SQLite File Strategy
|
|
137
|
+
|
|
138
|
+
**Mechanism**: Separate database file per tenant.
|
|
139
|
+
|
|
140
|
+
**Why file-based**: SQLite is single-file by design.
|
|
141
|
+
|
|
142
|
+
**Use case**: Testing and development only. Concurrent writes cause locking issues.
|
|
143
|
+
|
|
144
|
+
## Memory Management
|
|
145
|
+
|
|
146
|
+
### PostgreSQL (Shared Pool)
|
|
147
|
+
- Constant base: ~50MB for connection pool
|
|
148
|
+
- Growth: Only schema metadata (minimal)
|
|
149
|
+
- Scales to: 100+ tenants easily
|
|
150
|
+
|
|
151
|
+
### MySQL (Pool Per Tenant)
|
|
152
|
+
- Base per tenant: ~20MB connection pool
|
|
153
|
+
- Growth: Linear with active tenant count
|
|
154
|
+
- Consider: LRU cache for connection pools (not implemented in v3)
|
|
155
|
+
|
|
156
|
+
## Thread Safety Analysis
|
|
157
|
+
|
|
158
|
+
### What's Safe
|
|
159
|
+
|
|
160
|
+
**Multi-threaded request handling**: Each thread gets isolated adapter instance via `Thread.current`.
|
|
161
|
+
|
|
162
|
+
**Concurrent tenant access**: Thread 1 can be in tenant_a while Thread 2 is in tenant_b without interference.
|
|
163
|
+
|
|
164
|
+
**Background jobs**: Sidekiq workers are threads, get their own adapters.
|
|
165
|
+
|
|
166
|
+
### What's Unsafe
|
|
167
|
+
|
|
168
|
+
**Fiber switching**: Fibers within a thread share `Thread.current`. Fiber-based async (EventMachine, async gem) will have cross-contamination.
|
|
169
|
+
|
|
170
|
+
**Manual thread pooling with shared state**: Don't share adapter instances across threads.
|
|
171
|
+
|
|
172
|
+
**Solution**: v4 refactor uses `ActiveSupport::CurrentAttributes` which is fiber-safe.
|
|
173
|
+
|
|
174
|
+
## Error Handling Philosophy
|
|
175
|
+
|
|
176
|
+
### Fail Fast vs Graceful Degradation
|
|
177
|
+
|
|
178
|
+
**Tenant not found**: Raise exception. Why? Better to show error than serve wrong data.
|
|
179
|
+
|
|
180
|
+
**Tenant creation collision**: Raise exception. Why? Concurrent creation attempts indicate application bug.
|
|
181
|
+
|
|
182
|
+
**Rollback failure**: Fall back to default tenant. Why? Better to serve default data than crash entire request.
|
|
183
|
+
|
|
184
|
+
**Configuration errors**: Raise on boot. Why? Invalid config should prevent startup, not cause runtime failures.
|
|
185
|
+
|
|
186
|
+
## Excluded Models - Design Rationale
|
|
187
|
+
|
|
188
|
+
**Problem**: Some models (User, Company) exist globally, not per-tenant.
|
|
189
|
+
|
|
190
|
+
**Solution**: Establish separate connections that bypass tenant switching.
|
|
191
|
+
|
|
192
|
+
**Implementation**: PostgreSQL explicitly qualifies table names (`public.users`). MySQL uses separate connection.
|
|
193
|
+
|
|
194
|
+
**Why not conditional logic?**: Separate connections are cleaner than "if excluded, do X else do Y" throughout codebase.
|
|
195
|
+
|
|
196
|
+
**Limitation**: `has_and_belongs_to_many` doesn't work with excluded models. Must use `has_many :through` instead.
|
|
197
|
+
|
|
198
|
+
**See**: `AbstractAdapter#process_excluded_models` method
|
|
199
|
+
|
|
200
|
+
## Configuration Design
|
|
201
|
+
|
|
202
|
+
### Why Callable tenant_names?
|
|
203
|
+
|
|
204
|
+
**Problem**: Static arrays become stale as tenants are created/deleted.
|
|
205
|
+
|
|
206
|
+
**Solution**: Accept proc/lambda that queries database dynamically.
|
|
207
|
+
|
|
208
|
+
**Trade-off**: Extra query on each access. Consider caching.
|
|
209
|
+
|
|
210
|
+
### Why Hash Format for Multi-Server?
|
|
211
|
+
|
|
212
|
+
**Problem**: Different tenants might live on different database servers.
|
|
213
|
+
|
|
214
|
+
**Solution**: Hash maps tenant name to full connection config.
|
|
215
|
+
|
|
216
|
+
**Benefit**: Enables horizontal scaling and geographic distribution.
|
|
217
|
+
|
|
218
|
+
**See**: README.md examples and `Apartment.db_config_for` method
|
|
219
|
+
|
|
220
|
+
## Performance Design Decisions
|
|
221
|
+
|
|
222
|
+
### Why Query Cache Preservation?
|
|
223
|
+
|
|
224
|
+
**Impact**: 10-30% performance improvement on cache-heavy workloads.
|
|
225
|
+
|
|
226
|
+
**Cost**: Extra bookkeeping on every switch.
|
|
227
|
+
|
|
228
|
+
**Decision**: Worth it. Query cache is critical for Rails performance.
|
|
229
|
+
|
|
230
|
+
### Why Connection Verification?
|
|
231
|
+
|
|
232
|
+
**call to verify!**: Ensures connection is live after establishment.
|
|
233
|
+
|
|
234
|
+
**Why needed**: Stale connections from pool can cause mysterious failures.
|
|
235
|
+
|
|
236
|
+
**Cost**: Extra network round-trip, but prevents worse failures.
|
|
237
|
+
|
|
238
|
+
## Extension Points
|
|
239
|
+
|
|
240
|
+
### For Users
|
|
241
|
+
|
|
242
|
+
1. **Custom elevators**: Subclass `Generic`, override `parse_tenant_name`
|
|
243
|
+
2. **Callbacks**: Hook into `:create` and `:switch` events
|
|
244
|
+
3. **Custom adapters**: Subclass `AbstractAdapter` for new databases
|
|
245
|
+
|
|
246
|
+
### Design Principle
|
|
247
|
+
|
|
248
|
+
**Open for extension, closed for modification**: Users can add behavior without changing gem code.
|
|
249
|
+
|
|
250
|
+
## Limitations & Known Issues
|
|
251
|
+
|
|
252
|
+
### v3 Constraints
|
|
253
|
+
|
|
254
|
+
1. **Thread-local only**: Not fiber-safe
|
|
255
|
+
2. **Single adapter type**: Can't mix PostgreSQL schemas and MySQL databases in one app
|
|
256
|
+
3. **No horizontal sharding**: Each adapter connects to single database cluster
|
|
257
|
+
4. **Global excluded models**: Can't have different exclusions per tenant
|
|
258
|
+
|
|
259
|
+
### Why These Exist
|
|
260
|
+
|
|
261
|
+
Historical decisions made before newer Rails features (sharding, CurrentAttributes) existed.
|
|
262
|
+
|
|
263
|
+
### v4 Improvements
|
|
264
|
+
|
|
265
|
+
The `man/spec-restart` branch refactor addresses most limitations via connection-pool-per-tenant architecture.
|
|
266
|
+
|
|
267
|
+
## References
|
|
268
|
+
|
|
269
|
+
- Main module: `lib/apartment.rb`
|
|
270
|
+
- Public API: `lib/apartment/tenant.rb`
|
|
271
|
+
- Adapters: `lib/apartment/adapters/*.rb`
|
|
272
|
+
- Elevators: `lib/apartment/elevators/*.rb`
|
|
273
|
+
- Thread storage: Ruby documentation on `Thread.current`
|
|
274
|
+
- Rails connection pooling: Rails guides
|
data/docs/elevators.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Apartment Elevators - Middleware Design
|
|
2
|
+
|
|
3
|
+
**Key files**: `lib/apartment/elevators/*.rb`
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Elevators are Rack middleware that automatically detect tenant from HTTP requests and establish tenant context before application code runs.
|
|
8
|
+
|
|
9
|
+
**Name metaphor**: Like elevators transport you between building floors, these middleware transport requests between tenant contexts.
|
|
10
|
+
|
|
11
|
+
## Design Decision: Why Middleware?
|
|
12
|
+
|
|
13
|
+
**Problem**: Manual tenant switching in controllers is error-prone. Easy to forget, creates boilerplate.
|
|
14
|
+
|
|
15
|
+
**Solution**: Rack middleware intercepts all requests, switches tenant automatically based on request attributes.
|
|
16
|
+
|
|
17
|
+
**Trade-off**: Adds middleware overhead (minimal) but eliminates entire class of bugs.
|
|
18
|
+
|
|
19
|
+
## Critical Positioning Requirement
|
|
20
|
+
|
|
21
|
+
**Rule**: Elevators MUST be positioned before session/authentication middleware.
|
|
22
|
+
|
|
23
|
+
**Why**: Session data is tenant-specific. Loading session before establishing tenant context causes data leakage.
|
|
24
|
+
|
|
25
|
+
**How to verify**: `Rails.application.middleware` lists order. Elevator should appear before `ActionDispatch::Session` and `Warden::Manager`.
|
|
26
|
+
|
|
27
|
+
**See**: Configuration examples in README.md
|
|
28
|
+
|
|
29
|
+
## Available Elevator Strategies
|
|
30
|
+
|
|
31
|
+
**Files**: All in `lib/apartment/elevators/`
|
|
32
|
+
|
|
33
|
+
### Subdomain Elevator
|
|
34
|
+
|
|
35
|
+
**File**: `subdomain.rb`
|
|
36
|
+
|
|
37
|
+
**Strategy**: Extract first subdomain as tenant name.
|
|
38
|
+
|
|
39
|
+
**Why PublicSuffix gem?**: Handles international TLDs correctly. `example.co.uk` has TLD `.co.uk`, not just `.uk`.
|
|
40
|
+
|
|
41
|
+
**Exclusion mechanism**: Configurable list of ignored subdomains (www, admin, api). Returns nil for excluded, which uses default tenant.
|
|
42
|
+
|
|
43
|
+
**Why class-level exclusions?**: Shared across all instances. Set once in initializer.
|
|
44
|
+
|
|
45
|
+
### Domain Elevator
|
|
46
|
+
|
|
47
|
+
**File**: `domain.rb`
|
|
48
|
+
|
|
49
|
+
**Strategy**: Use domain name (excluding www and TLD) as tenant.
|
|
50
|
+
|
|
51
|
+
**Use case**: When domain itself identifies tenant (acme.com vs widgets.com), not subdomain.
|
|
52
|
+
|
|
53
|
+
### Host Elevator
|
|
54
|
+
|
|
55
|
+
**File**: `host.rb`
|
|
56
|
+
|
|
57
|
+
**Strategy**: Use full hostname as tenant name.
|
|
58
|
+
|
|
59
|
+
**Ignored subdomains**: Optional configuration to strip www/app from beginning.
|
|
60
|
+
|
|
61
|
+
**Use case**: Custom domains where full hostname matters.
|
|
62
|
+
|
|
63
|
+
### HostHash Elevator
|
|
64
|
+
|
|
65
|
+
**File**: `host_hash.rb`
|
|
66
|
+
|
|
67
|
+
**Strategy**: Direct hash mapping from hostname to tenant name.
|
|
68
|
+
|
|
69
|
+
**Why needed?**: When hostname→tenant mapping is arbitrary or complex.
|
|
70
|
+
|
|
71
|
+
**Trade-off**: Requires explicit configuration per tenant. Not dynamic.
|
|
72
|
+
|
|
73
|
+
### Generic Elevator
|
|
74
|
+
|
|
75
|
+
**File**: `generic.rb`
|
|
76
|
+
|
|
77
|
+
**Purpose**: Base class for custom elevators. Accept Proc for inline logic or subclass for complex scenarios.
|
|
78
|
+
|
|
79
|
+
**Extension point**: Override `parse_tenant_name(request)` method.
|
|
80
|
+
|
|
81
|
+
**See**: Examples in file comments
|
|
82
|
+
|
|
83
|
+
## Design Patterns
|
|
84
|
+
|
|
85
|
+
### Why Return nil for Excluded?
|
|
86
|
+
|
|
87
|
+
Returning nil (not default_tenant name) allows Apartment core to handle fallback logic. Separation of concerns.
|
|
88
|
+
|
|
89
|
+
### Why ensure Block in call()?
|
|
90
|
+
|
|
91
|
+
Guarantees tenant cleanup even if application code raises. Prevents request bleeding into next request's tenant context.
|
|
92
|
+
|
|
93
|
+
### Why Rack::Request Object?
|
|
94
|
+
|
|
95
|
+
Standard interface. Access to host, headers, session, cookies. Database-independent.
|
|
96
|
+
|
|
97
|
+
## Request Lifecycle
|
|
98
|
+
|
|
99
|
+
**Sequence**:
|
|
100
|
+
1. Rack request enters application
|
|
101
|
+
2. Elevator middleware intercepts (positioned early)
|
|
102
|
+
3. Calls `parse_tenant_name(request)` - strategy determines tenant
|
|
103
|
+
4. Calls `Apartment::Tenant.switch(tenant) { @app.call(env) }`
|
|
104
|
+
5. Application processes in tenant context
|
|
105
|
+
6. Ensure block resets tenant after response
|
|
106
|
+
|
|
107
|
+
**Critical**: Step 6 happens even on exceptions. Why? Prevent tenant leakage.
|
|
108
|
+
|
|
109
|
+
## Performance Considerations
|
|
110
|
+
|
|
111
|
+
### Caching Tenant Lookups
|
|
112
|
+
|
|
113
|
+
If `parse_tenant_name` does database queries, consider caching:
|
|
114
|
+
- Subdomain→tenant mapping cached for 5-10 minutes
|
|
115
|
+
- Invalidate cache when tenants created/deleted
|
|
116
|
+
|
|
117
|
+
**Why needed?**: Elevator runs on EVERY request. Database query per request adds latency.
|
|
118
|
+
|
|
119
|
+
**Not implemented in v3**: Users must implement caching in custom elevators.
|
|
120
|
+
|
|
121
|
+
### Why Not Cache in Gem?
|
|
122
|
+
|
|
123
|
+
Different applications have different caching strategies (Redis, Memcached, Rails.cache). Prescribing one limits flexibility.
|
|
124
|
+
|
|
125
|
+
## Error Handling Philosophy
|
|
126
|
+
|
|
127
|
+
**Default behavior**: Exceptions propagate. TenantNotFound crashes request.
|
|
128
|
+
|
|
129
|
+
**Rationale**: Better to show error than serve wrong data or default data without user realizing.
|
|
130
|
+
|
|
131
|
+
**Alternative**: Custom elevator can rescue and return 404/redirect.
|
|
132
|
+
|
|
133
|
+
**See**: docs/adapters.md for error hierarchy
|
|
134
|
+
|
|
135
|
+
## Extension Points
|
|
136
|
+
|
|
137
|
+
### Creating Custom Elevators
|
|
138
|
+
|
|
139
|
+
**Two approaches**:
|
|
140
|
+
|
|
141
|
+
1. **Inline Proc**: For simple logic, pass Proc to Generic
|
|
142
|
+
2. **Subclass**: For complex logic, override `parse_tenant_name`
|
|
143
|
+
|
|
144
|
+
**When to subclass**:
|
|
145
|
+
- Multi-strategy fallback (header → session → subdomain)
|
|
146
|
+
- Database lookups with caching
|
|
147
|
+
- Complex validation/transformation logic
|
|
148
|
+
|
|
149
|
+
**See**: `generic.rb` for base implementation
|
|
150
|
+
|
|
151
|
+
### Common Custom Patterns
|
|
152
|
+
|
|
153
|
+
**Header-based**: API requests with `X-Tenant-ID` header
|
|
154
|
+
**Session-based**: Tenant selected in login flow, stored in session
|
|
155
|
+
**API key-based**: Database lookup from authentication token
|
|
156
|
+
**Hybrid**: Try multiple strategies in priority order
|
|
157
|
+
|
|
158
|
+
## Common Pitfalls
|
|
159
|
+
|
|
160
|
+
### Pitfall: Elevator After Session Middleware
|
|
161
|
+
|
|
162
|
+
**Symptom**: Wrong tenant's session data appearing
|
|
163
|
+
|
|
164
|
+
**Cause**: Session loaded before tenant switched
|
|
165
|
+
|
|
166
|
+
**Fix**: Reposition elevator before session middleware
|
|
167
|
+
|
|
168
|
+
### Pitfall: Database Queries in parse_tenant_name
|
|
169
|
+
|
|
170
|
+
**Symptom**: Slow request times, database overload
|
|
171
|
+
|
|
172
|
+
**Cause**: Query on every request without caching
|
|
173
|
+
|
|
174
|
+
**Fix**: Implement caching layer
|
|
175
|
+
|
|
176
|
+
### Pitfall: Not Handling Exclusions
|
|
177
|
+
|
|
178
|
+
**Symptom**: www.example.com creates "www" tenant, admin pages switch tenants
|
|
179
|
+
|
|
180
|
+
**Cause**: No exclusion configuration
|
|
181
|
+
|
|
182
|
+
**Fix**: Configure `excluded_subdomains`
|
|
183
|
+
|
|
184
|
+
### Pitfall: Returning Tenant Name That Doesn't Exist
|
|
185
|
+
|
|
186
|
+
**Symptom**: TenantNotFound errors
|
|
187
|
+
|
|
188
|
+
**Cause**: No validation before switching
|
|
189
|
+
|
|
190
|
+
**Fix**: Add existence check in custom elevator or handle error
|
|
191
|
+
|
|
192
|
+
## Testing Elevators
|
|
193
|
+
|
|
194
|
+
**Challenge**: Elevators are middleware, not models/controllers.
|
|
195
|
+
|
|
196
|
+
**Solution**: Use `Rack::MockRequest` to simulate requests with different hosts.
|
|
197
|
+
|
|
198
|
+
**Pattern**: Mock `Apartment::Tenant.switch` to verify correct tenant extracted.
|
|
199
|
+
|
|
200
|
+
**See**: `spec/unit/elevators/` for examples
|
|
201
|
+
|
|
202
|
+
## Integration with Background Jobs
|
|
203
|
+
|
|
204
|
+
**Important**: Elevators only affect web requests. Background jobs need separate tenant handling.
|
|
205
|
+
|
|
206
|
+
**Solution**: Job frameworks must capture and restore tenant (apartment-sidekiq gem).
|
|
207
|
+
|
|
208
|
+
**Why separate?**: Jobs aren't HTTP requests. No Rack middleware involved.
|
|
209
|
+
|
|
210
|
+
## Multi-Elevator Setup
|
|
211
|
+
|
|
212
|
+
**Possible but discouraged**: Multiple elevators in middleware stack.
|
|
213
|
+
|
|
214
|
+
**Why discouraged**: Last elevator wins. Complex, hard to debug.
|
|
215
|
+
|
|
216
|
+
**Alternative**: Single custom elevator with multi-strategy logic.
|
|
217
|
+
|
|
218
|
+
## References
|
|
219
|
+
|
|
220
|
+
- Generic base: `lib/apartment/elevators/generic.rb`
|
|
221
|
+
- Subdomain implementation: `lib/apartment/elevators/subdomain.rb`
|
|
222
|
+
- Domain implementation: `lib/apartment/elevators/domain.rb`
|
|
223
|
+
- Host implementations: `lib/apartment/elevators/host.rb`, `host_hash.rb`
|
|
224
|
+
- First subdomain: `lib/apartment/elevators/first_subdomain.rb`
|
|
225
|
+
- Rack middleware: https://github.com/rack/rack/wiki/Middleware
|
|
226
|
+
- PublicSuffix gem: https://github.com/weppos/publicsuffix-ruby
|
|
Binary file
|