Saturday, June 21, 2025
HomeRuby On RailsMulti tenant purposes with horizontal sharding and Rails Occasion Retailer

Multi tenant purposes with horizontal sharding and Rails Occasion Retailer


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.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments