Horizontal sharding has been launched in Rails 6.0 (simply the API, with later additions to help automated shard choice). This permits us to simply construct multi tenant purposes with tenant’s knowledge separated in numerous databases. On this submit I’ll discover easy methods to construct such an app with separate occasion retailer knowledge for every tenant.
Utility thought
Let’s sketch some non-functional necessities for our pattern software.
- First: all tenant knowledge is separated right into a shard database,
- Second: tenant’s administration, shared knowledge, cache & queues use a single shared database (admin’s software),
- Third: embrace async, all occasion handlers will likely be async and carried out utilizing Stable Queue,
- Fourth: every tenant makes use of separate area.
And one last item … the tenant database setup will likely be static, outlined in config/database.yml
file. This implies so as to add a brand new tenant requires database setup, config file replace & software deployment.
So after producing new Rails 8 software let’s make it work.
Database configuration
Rails documentation offers an excellent description of easy methods to configure your software to make use of horizontal sharding.
What it’s a must to do is to outline all shards (bear in mind the major
one for the default
shard, which we’ll use for the admin knowledge) in every of the applying environments.
Our software’s config/database.yml
file will appear to be this:
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
growth:
major:
<<: *default
database: storage/growth.sqlite3
migrations_paths: db/migrate
cache:
<<: *default
database: storage/development_cache.sqlite3
migrations_paths: db/cache_migrate
queue:
<<: *default
database: storage/development_queue.sqlite3
migrations_paths: db/queue_migrate
cable:
<<: *default
database: storage/development_cable.sqlite3
migrations_paths: db/cable_migrate
arkency:
<<: *default
database: storage/growth.arkency.sqlite3
migrations_paths: db/shards
railseventstore:
<<: *default
database: storage/growth.railseventstore.sqlite3
migrations_paths: db/shards
Bear in mind to specify migration paths, otherwise you’ll find yourself along with your schema tousled.
Database schema
To create/setup/migrate the database you employ the standard Rails database duties.
By default the Rails duties run for all shards. If you wish to run it on a single shard simply add a shard identify after :
to the duty identify, like within the instance:
bin/rails db:migrate:<shard_name>
To generate migrations for a selected shard use the --database
parameter to specify the shard the place the generated migration needs to be executed:
bin/rails generate migration XXXX --database=<shard_name>
Outline fashions
As a result of we’ve a mixture of ActiveRecord fashions, some reaching the major
shard for tenant’s & shared knowledge, and others used solely to learn/write to particular shards (simply tenant’s enterprise knowledge) we have to outline totally different base summary courses for this 2 sorts of mannequin courses.
Mark you “default” base class with primary_abstract_class
and units it to all the time connect with the major
database (default
shard).
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
connects_to database: { writing: :major, studying: :major }
finish
For the sharded fashions we’ve to outline the bottom class as:
class ShardRecord < ApplicationRecord
self.abstract_class = true
connects_to shards: {
arkency: { writing: :arkency, studying: :arkency },
railseventstore: { writing: :railseventstore, studying: :railseventstore }
}
finish
Bear in mind so as to add an entry right here when a brand new tenant is outlined.
Rails Occasion Retailer setup
Having outlined the shard and an summary base class that enables us to connect with a particular shard (how it’s chosen will likely be defined beneath) we may setup our RailsEventStore::Shopper
occasion.
There are three issues to note within the RES setup that aren’t typical ones.
1st: Occasion repository
We now have to outline the RES repository for instance of RubyEventStore::ActiveRecord::EventRepository
with particular model_factory
. The ShardRecord
needs to be used right here because the summary base class. This may permit RES to connect with a selected shard database.
2nd: Asynchronous dispatcher
Within the RES documentation the RailsEventStore::AfterCommitAsyncDispatcher
is advisable for use if you wish to deal with area occasions asynchronously. Nonetheless since Rails has launched enqueue_after_transaction_commit
within the ActiveJob::Base
all we want is:
class ApplicationJob < ActiveJob::Base
self.enqueue_after_transaction_commit = true
finish
after which simply RubyEventStore::ImmediateAsyncDispatcher
with RailsEventStore::ActiveJobScheduler
is sufficient.
… which may be very handy, as RailsEventStore::AfterCommitAsyncDispatcher
doesn’t test what database it’s related to 🙁
third: Retailer shard in area occasion’s metadata
The app makes use of an automated shard selector & resolver (Rails function). Nonetheless this works solely throughout the scope of an internet request. All asynchronously executed software jobs, scripts, and so forth have to manually choose the legitimate shard. That’s why the present shard is saved in every area occasion’s metadata.
This info will likely be used within the occasion handlers. First to connect with the legitimate shard – the identical because the dealt with area occasion. Second to arrange the RES consumer’s metadata to maintain the shard info in all area occasions printed by the occasion handler. That is carried out within the ShardedHandler
module.
module ShardedHandler
def carry out(occasion)
shard = occasion.metadata[:shard] || :default
ActiveRecord::Base.connected_to(position: :writing, shard: shard.to_sym) do
Rails
.configuration
.event_store
.with_metadata(shard: shard) { tremendous }
finish
finish
finish
RES setup
The whole setup of RailsEventStore::Shopper
appears like this:
Rails.configuration.to_prepare do
Rails.configuration.event_store = RailsEventStore::Shopper.new(
repository: RubyEventStore::ActiveRecord::EventRepository.new(
model_factory: RubyEventStore::ActiveRecord::WithAbstractBaseClass.new(ShardRecord),
serializer: JSON,
),
dispatcher: RubyEventStore::ComposedDispatcher.new(
RubyEventStore::ImmediateAsyncDispatcher.new(
scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)
),
RubyEventStore::Dispatcher.new
),
request_metadata: ->(env) do
request = ActionDispatch::Request.new(env)
{ remote_ip: request.remote_ip, request_id: request.uuid, shard: ShardRecord.current_shard.to_s }
finish,
)
finish
Computerized shard choice
Rails documentation describes how the Rails framework permits to outline automated database/shard choice.
First generate an initializer class utilizing:
bin/rails g active_record:multi_db
Within the pattern software we have to modify it to match our wants.
Rails.software.configure do
config.active_record.shard_selector = { lock: false, class: "ShardRecord" }
config.active_record.shard_resolver = ->(request)
finish
We set shard_selector
to make use of the ShardRecord
class because the supply of details about outlined shards. The lock: false
permits the applying to learn from a number of shards on the identical time (we have to manually deal with that – particulars easy methods to do it are described in Rails documentation).
Rails documentation advises:
For tenant primarily based sharding, lock ought to all the time be true to stop software code from mistakenly switching between tenants.
Nonetheless we’ve the requirement to maintain shared knowledge in a separate database and solely hold tenant’s enterprise knowledge in its shards. That’s why we permit switching shards.
The shard_resolver
may be very easy right here – simply use the request’s host identify to seek out the tenant in shared database after which use its shard
methodology to seek out out which shard the tenant’s knowledge needs to be saved or learn from.
Occasion handlers
Rails handles shard choice for us throughout an internet request. However as I’ve talked about earlier than this won’t work in asynchronously processed software jobs – occasion handlers. RailsEventStore already defines async handler helper modules RailsEventStore::AsyncHandler
to deal with area occasion asynchronously and RailsEventStore::CorrelatedHandler
to make sure traceability by defining correlation & causation ids for printed occasions. By defining (above) the ShardedHandler
module we make sure the occasion handler code is executed utilizing a sound shard connection and that every printed area occasion will nonetheless have shard info in occasion’s metadata.
class LogVisitsByIp < ApplicationJob
prepend ShardedHandler
prepend RailsEventStore::CorrelatedHandler
prepend RailsEventStore::AsyncHandler
def carry out(occasion)
...
finish
finish
By prepending the occasion handler’s code with this 3 modules we’ve clear code, free from infrastructure “plumbing”.
Utilizing SolidQueue
As a result of we’ve software jobs executing utilizing a tenant’s shard connection and we wish to keep away from separate queues for every tenant the setup of queue connections should be outlined:
It’s set in config/environments/<env>.rb
information. For every shard (together with default) we’ve to outline the database the place SolidQueue will connect with.
...
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = {
shards: {
default: { writing: :queue },
arkency: { writing: :queue },
railseventstore: { writing: :queue }
}
}
...
Abstract
The whole pattern software for this submit will be present in RES examples repository. You may also like my earlier submit, with totally different method of separating knowledge in Rails software.