Skip to main content

architecture-notes

Processing flow

Our current form responses in RDS Postgress are processed by FormResponse::ManagerWorker in Sidekiq. The general flow looks like this:

  • app_form_response#create endpoint creates a record in raw_app_form_responses table
  • FormResponse::ManagerWorker is running recursively and takes just added records from raw_app_form_responses table and runs for these records FormResponse::BatchFormResponseWorker
  • FormResponse::BatchFormResponseWorker creates record in app_form_responses table and runs the integrations for this form response
  • The record gets deleted from raw_app_form_responses table after it's processed

We have decided to add additional FormResponse::ManagerWorkerPs worker for processing form responses from PlanetScale. Generally the processing flow of FormResponse::ManagerWorkerPs is similar except of used tables. The records are inserted to FormResponse table in PlanetScale by Next.js /form/responses endpoint. As a result our users can see their form responses immediately. We don't have raw buffer table in PlanetScale and we save the response ids for integrations processing in table FormResponseQueue. The FormResponseQueue is used by FormResponse::ManagerWorkerPs to asynchronously run integrations for the form responses.

The migration story

As soon as we are going to use PlanetScale for our form responses, we need to migrate our existing data to the new database. It cannot be done by running a single migration, because the tables are huge and new records are coming all the time. In this regard we are going to move data from Amazon RDS to PlanetScale gradually. We will group the data by app_id and move them together in several runs.

PlanetScale vs non-PlanetScale apps

As a result of mentioned migrations we have PlanetScale apps which are using new DB and non-PlanetScale apps.

Our App model now has a planet_scale method, which returns true for the PlanetScale formBuilder apps. It uses the following ENVs to determine the PlanetScale apps:

  • PLANET_SCALE_APPS_IDS - PlanetScale app ids delimited by comma ,
  • PLANET_SCALE_APPS_START_ID - the app is PlanetScale if app_id >= PLANET_SCALE_APPS_START_ID

All the PlanetScale apps have window.PLANET_SCALE variable on the front side. It helps us to determine the endpoint which should be triggered on backend:

  • window.PLANET_SCALE = 'on' - trigger Next.js /form/responses endpoint
  • otherwise trigger Rails app_form_response#create endpoint

About Sequel models

Our existing ActiveRecord models are connected RDS Postgres database. The Sequel gem has models which are similar to the ones in ActiveRecord. These models will be connected to the PlanetScale database in our project.

We introduce new PlanetScale::PendingTransaction, PlanetScale::FormResponse, PlanetScale::PaymentNotification Sequel models.

These models use global variable $db_ps to connect to the PlanetScale DB. $db_ps is defined in application.rb and contains Sequel connection pool. The pool is created with default settings but could be changed by using following ENVs:

  • PS_DB_POOL_MAX_CONNECTIONS - pool size
  • PS_DB_POOL_TIMEOUT - the amount of seconds to wait to acquire a connection before raising a PoolTimeoutError
  • PS_DB_CONNECTION - PlanetScale connection string

ActiveRecord model updates

As soon as we move our data to PlanetScale, some of our existing ActiveRecord associations will stop working. To solve this problem we created appropriate methods with the same names inside of our AR models. This is an example of such association-like method from App model:

def app_form_responses
return super unless planet_scale?
PlanetScale::FormResponse.where(app_id: id)
end

How to make less code updates in powr project

As soon as ActiveRecord PendingTransaction, AppFormResponse, PaymentNotification models are used in many places of our project, it's a challenge to update the code for both Sequel/ActiveRecord models across the whole project.

We prepared special wrapper classes to get the appropriate models depending on the app. It helps to reduce the updates across the whole project. The wrapper classes look like this:

class Wrapper::FormResponse
def self.get_model(params)
app = params[:app].present? ? params[:app] : App.cached_find_by("id", params[:app_id])
return PlanetScale::FormResponse if app.planet_scale?
AppFormResponse
end
end

We can connect to correct database by using the following code: Wrapper::FormResponse.get_model(app: @app)

Another problem is Sequel method names. ActiveRecord and Sequel methods names are different sometimes. That's why initially it used to be necessary to update the code in many places of the project. We created SequelActiveRecord module and included it to all our Sequel models to reduce the code updates. SequelActiveRecord module contains the methods which have the same names as in ActiveRecord but use Sequel API. As a result we can use the existing ActiveRecord method names while using Sequel models. E.g. we can do like this:

Wrapper::FormResponse.get_model(app: @app).find(params[:id]) 

It should work regardless of the model returned by Wrapper::FormResponse.