Skip to content

Commit 60b0ba2

Browse files
committed
doc: GUIDE.md
combined to-do list and documentation. as the to-do list gets shorter, the documentation will get more fleshed-out.
1 parent cce092f commit 60b0ba2

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

GUIDE.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Active Record Tenanting
2+
3+
This file will eventually become a complete "Rails Guide"-style document explaining Active Record tenanting with this gem.
4+
5+
In the meantime, it is a work-in-progress containing:
6+
7+
- skeleton outline for documentation
8+
- functional roadmap represented as to-do checklists
9+
10+
11+
## Introduction
12+
13+
Documentation:
14+
15+
> [!TIP]
16+
> If you're not familiar with how Rails's built-in horizontal sharding works, it may be worth reading the Rails Guide on [Multiple Databases with Active Record](https://guides.rubyonrails.org/active_record_multiple_databases.html#setting-up-your-application) before proceeding.
17+
18+
- this gem primarily extends Active Record,
19+
- essentially creating a new Connection Pool for each tenant,
20+
- and extending horizontal shard swapping to support these pools.
21+
- also provides test helpers to make it easy to handle tenanting in your test suite
22+
- but also touches many other parts of Rails
23+
- integrations for Middleware, Action View Caching, Active Job, Action Cable, Active Storage, Action Mailbox, and Action Text
24+
- support and documentation for Solid Cache, Solid Queue, Solid Cable, and Turbo Rails
25+
- a Tenant is just a string that is used for:
26+
- the sqlite database filename
27+
- the subdomain (or path element)
28+
- fragment cache disambiguation
29+
- global id disambiguation
30+
- talk a bit about busted assumptions about shared state
31+
- database ids are no longer unique
32+
- global ids are no longer global
33+
- cache is no longer global
34+
- and what we do in this gem to help manage "shard" state
35+
36+
37+
## Active Record
38+
39+
### Configuration
40+
41+
Documentation:
42+
- how to configure database.yml for tenanting a primary database
43+
- how to configure database.yml for tenanting a non-primary database
44+
- how to make a class that inherits from ActiveRecord::Base "sublet" from a tenanted database
45+
- and note how we do it out of the box for Rails records
46+
- how to run database tasks and what's changed
47+
- demonstrate how to configure an app for subdomain tenants
48+
- app.config.hosts
49+
- example TenantSelector proc
50+
51+
TODO:
52+
- implement `AR::Tenanted::DatabaseConfigurations::RootConfig` (name?)
53+
- [ ] `#database_path_for(tenant_name)`
54+
- [ ] `#tenants` returns all the tenants on disk (for iteration)
55+
56+
- implement `AR::Tenanted::DatabaseConfigurations::TenantConfig` (name?)
57+
- [ ] make sure the logs include the tenant name (via `#new_connection`)
58+
59+
- Active Record class methods
60+
- [ ] `.tenanted`
61+
- extends with `Base`
62+
- sets `Tenant.base_class=`
63+
- must only be set ONCE in the application
64+
- [ ] `.tenanted_with`
65+
- extends with `Sublet`
66+
- should error if self is not an abstract base class or if target is not tenanted abstract base class
67+
- is the name right? should we have to provide the name of the tenanted class?
68+
- [ ] `.tenanted?`
69+
- [ ] `.tenanted_class` nil or the abstract base class
70+
- [ ] all the creation and schema migration complications (we have existing tests for this)
71+
- think about race conditions here, maybe use a file lock to figure it out
72+
- running migrations (they are done in a transaction, but the second thread's migration may fail resulting in a 500?)
73+
- loading schemas (if the first thread loads the schema and inserts data, can the second thread accidentally drop/load causing data loss?)
74+
- [ ] feature to turn off automatic creation/migration
75+
- make sure we pay attention to Rails.config.active_record.migration_error when we turn off auto-migrating
76+
77+
- database tasks
78+
- [ ] make `db:migrate:tenants` iterate over all the tenants on disk
79+
- [ ] make `db:migrate AR_TENANT=asdf` run migrations on just that tenant
80+
- [ ] do that for all (?) the database tasks like `db:create`, `db:prepare`, `db:seeds`, etc.
81+
82+
- tenant selector
83+
- [ ] rebuild `AR::Tenanted::TenantSelector` to take a proc
84+
- make sure it sets the tenant and prohibits shard swapping
85+
- or explicitly untenanted, we allow shard swapping
86+
- or else 404s if an unrecognized tenant
87+
88+
- `Tenant`
89+
- `.current`
90+
- `.current=`
91+
- `.while_tenanted`
92+
- `.exist?`
93+
- `.all`
94+
- `.create`
95+
- think about race conditions here, maybe use a file lock to figure it out
96+
- `.destroy`
97+
- think about race conditions here, maybe use a file lock to figure it out
98+
- should delete the wal and shm files, too
99+
- we need to be close existing connections / statements / transactions(?)
100+
- relevant adapter code https://github.com/rails/rails/blob/91d456366638ac6c3f6dec38670c8ada5e7c69b1/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb#L23-L26
101+
- relevant issue/pull-request https://github.com/rails/rails/pull/53893
102+
103+
- installation
104+
- [ ] install a variation on the default database.yml with primary tenanted and non-primary "global" untenanted
105+
- initializer
106+
- [ ] install `TenantSelector` and configure it with a proc
107+
- [ ] commented line like `Tenant = ActiveRecord::Tenanted::Tenant`
108+
109+
- pruning connections and connection pools
110+
- [ ] look into whether the proposed Reaper changes will allow us to set appropriate connection min/max/timeouts
111+
- and if not, figure out how to prune unused/timed-out connections
112+
- [ ] we should also look into how to cap the number of connection pools, and prune them
113+
114+
115+
### Tenanting in your application
116+
117+
Documentation:
118+
- introduce the `Tenant` module
119+
- demonstrate how to create a tenant, destroy a tenant, etc.
120+
- troubleshooting: what errors you might see in your app and how to deal with it
121+
- specifically when running untenanted
122+
123+
124+
### Testing
125+
126+
Documentation:
127+
- explain the concept of a default tenant
128+
- explain `while_untenanted`
129+
130+
131+
TODO:
132+
- testing
133+
- [ ] set up test helper to default to a tenanted named "test-tenant"
134+
- [ ] set up test helpers to deal with parallelized tests, too (e.g. "test-tenant-19")
135+
- [ ] allow the creation of tenants within transactional tests if we can?
136+
- either by cleaning up properly (hard)
137+
- or by providing a test helper that does `ensure ... Tenant.destroy`
138+
- [ ] a `while_untenanted` test helper
139+
140+
141+
## Caching
142+
143+
Documentation:
144+
- explain why we need to be careful
145+
146+
TODO:
147+
- [ ] need to do some exploration on how to make sure all caching is tenanted
148+
- and then we can have belt-and-suspenders like we do with ActiveJob
149+
150+
151+
## Action View Fragment Caching
152+
153+
TODO:
154+
- [ ] extend `#cache_key` on Base
155+
- [ ] extend `#cache_key` on Sublet
156+
157+
158+
### Solid Cache
159+
160+
Documentation:
161+
- describe one-big-cache and cache-in-the-tenanted-database strategies
162+
- how to configure Solid Cache for one-big-cache
163+
- how to configure Solid Cache for tenanted-cache
164+
165+
TODO:
166+
- upstream
167+
- [ ] feature: make shard swap prohibition database-specific
168+
- which would work around Solid Cache config wonkiness caused by https://github.com/rails/solid_cache/pull/219
169+
170+
171+
## Active Job
172+
173+
Documentation:
174+
- explain why we need to be careful
175+
- explain belt-and-suspenders of
176+
- ActiveJob including the current tenant,
177+
- and any passed record being including the tenant in global_id
178+
179+
180+
TODO:
181+
- [ ] extend `to_global_id` and friends for Base
182+
- [ ] extend `to_global_id` and friends for Sublet
183+
- [ ] extend `ActiveJob` to set the tenant in `perform_now`
184+
185+
186+
## Active Storage
187+
188+
Documentation:
189+
- explain why we need to be careful
190+
- how to configure Disk Service so that each client is in a tenanted subdirectory
191+
- how to configure S3 so that each client is in a tenanted bucket
192+
193+
TODO:
194+
- [ ] still have to do some exploration here to figure out how best to tackle it
195+
- and then we can have belt-and-suspenders like we do with ActiveJob (hopefully)
196+
197+
198+
## Action Cable
199+
200+
Documentation:
201+
- explain why we need to be careful
202+
- how to make a channel "tenant safe"
203+
- identified_by
204+
- how the global id contains tenant also
205+
- do we need to document each adapter?
206+
- async
207+
- test
208+
- solid_cable
209+
- redis?
210+
211+
TODO:
212+
- [ ] explore if there's something we can/should do in Channel base case to automatically tenant
213+
- and then we can have belt-and-suspenders like we do with ActiveJob
214+
- [ ] understand action_cable_meta_tag
215+
- [ ] config.action_cable.log_tags set up with tenant?
216+
217+
218+
### Turbo Rails
219+
220+
Documentation:
221+
- explain why we need to be careful
222+
223+
TODO:
224+
- [ ] some testing around global id would be good here
225+
226+
227+
## ActionMailbox
228+
229+
TODO:
230+
- [ ] I need a use case here around mail routing before I tackle it

0 commit comments

Comments
 (0)