readyset 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a7eff87ba2d4dfc95a623d57486cefc986da1ef725d7fd4193b3611aa86b72be
4
+ data.tar.gz: fd8ce3ee3520d4943f759c716877d5ec82e185101403316529031346aec01d7d
5
+ SHA512:
6
+ metadata.gz: bf03429429fec9e83f2d7a82dd9985302865f724f59fe93612b862a78447a5a210f024dbca1235e11ac1ae28edb277b6ba0b712a770ed09a2905b78b5a4525e9
7
+ data.tar.gz: 6979425963dc743d812707ce5d2e89c871ba779c2c85de035afa8c285c68a188b25fe43991752937aff6edb01829097edbe2a46eb380c0c058b031f814be56a2
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require rails_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ inherit_from:
2
+ - .rubocop_airbnb.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.2
6
+ NewCops: disable
7
+
8
+ Style/StringLiterals:
9
+ EnforcedStyle: single_quotes
10
+ Enabled: true
11
+
12
+ Style/StringLiteralsInInterpolation:
13
+ EnforcedStyle: single_quotes
14
+ Enabled: true
15
+
@@ -0,0 +1,2 @@
1
+ require:
2
+ - rubocop-airbnb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in readyset.gemspec
6
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ReadySet Technology Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,383 @@
1
+ # ReadySet Rails
2
+
3
+ A gem for caching with [ReadySet](https://readyset.io) within Rails applications.
4
+
5
+ ![Build status](https://github.com/readysettech/readyset-rails/actions/workflows/rspec.yml/badge.svg)
6
+ [![Number of GitHub issues that are open](https://img.shields.io/github/issues/readysettech/readyset-rails)](https://github.com/readysettech/readyset-rails/issues)
7
+ ![Number of GitHub closed issues](https://img.shields.io/github/issues-closed/readysettech/readyset-rails)
8
+ ![Number of GitHub pull requests that are open](https://img.shields.io/github/issues-pr-raw/readysettech/readyset-rails)
9
+ ![GitHub release; latest by date](https://img.shields.io/github/v/release/readysettech/readyset-rails)
10
+ [![Slack](https://img.shields.io/badge/Join%20Slack-gray?logo=slack&logoColor=white)](https://join.slack.com/t/readysetcommunity/shared_invite/zt-2272gtiz4-0024xeRJUPGWlRETQrGkFw)
11
+ [![Follow us on X, formerly Twitter](https://img.shields.io/twitter/follow/ReadySet?style=social)](https://twitter.com/readysetio)
12
+
13
+ :star: If you find this gem useful, please consider giving us a star on GitHub! Your support helps us continue to innovate and deliver exciting new features.
14
+
15
+ ## Table of Contents
16
+ - [What is ReadySet?](#what-is-readyset)
17
+ - [What does this gem do?](#what-does-this-gem-do)
18
+ - [Installing](#installing)
19
+ - [Quickstart](#quickstart)
20
+ - [Usage](#usage)
21
+ - [Getting Started with Caching](#getting-started-with-caching)
22
+ - [Query Routing in Controllers](#query-routing-in-controllers)
23
+ - [Query Routing in Models](#query-routing-in-models)
24
+ - [Cache Migrations](#cache-migrations)
25
+ - [Automatic Failover](#automatic-failover)
26
+ - [Configuration Options](#configuration-options)
27
+ - [License](#license)
28
+
29
+ ## What is ReadySet?
30
+
31
+ [ReadySet](https://readyset.io) is a database acceleration engine that acts as
32
+ a read replica and implements a novel type of query caching that automatically
33
+ keeps your caches up-to-date by watching your database's replication stream.
34
+ ReadySet helps you scale your database by shedding load and reducing query
35
+ latency.
36
+
37
+ ## What does this gem do?
38
+
39
+ This gem makes it easy to use ReadySet from within your Rails application by
40
+ allowing you to selectively route queries to ReadySet. The high-level features
41
+ of this gem include:
42
+
43
+ - A `Readyset.route` method that takes a block and routes to ReadySet any
44
+ queries within that block
45
+ - A controller extension that allows you to route to ReadySet all queries
46
+ that occur within specific controller actions
47
+ - A model extension that allows you to define queries to be routed to ReadySet
48
+ within the context of an existing model
49
+ - Rake tasks that allow you to easily manage cache migration on ReadySet,
50
+ ensuring that a consistent set of caches exists across all of your
51
+ environments
52
+ - Automatic failover back to your primary database in the event that your
53
+ ReadySet instance is unreachable (disabled by default)
54
+
55
+ **Note that ReadySet only guarantees support for PostgreSQL right now, so this
56
+ gem only supports PostgreSQL.**
57
+
58
+ ## Installing
59
+
60
+ If you run into any trouble with the below steps, please feel free to reach
61
+ out via our [community Slack](https://join.slack.com/t/readysetcommunity/shared_invite/zt-2272gtiz4-0024xeRJUPGWlRETQrGkFw)!
62
+
63
+ 1. Follow the instructions [here](https://docs.readyset.io/get-started/install-rs/postgres)
64
+ to install and run ReadySet
65
+ 2. Add the following line to your Gemfile and run `bundle install`:
66
+ ```sh
67
+ gem 'readyset'
68
+ ```
69
+ The ReadySet Rails gem currenly supports Ruby versions >= 3.0 and Rails
70
+ versions >= 6.1.
71
+ 3. Add a section to your `config/database.yml` file with ReadySet's connection
72
+ information. If you currently connect to only one database, you'll need to
73
+ move your primary database connection information to be nested under a new
74
+ key named `primary`.
75
+ ```yaml
76
+ development:
77
+ primary:
78
+ # This is the connection information for your primary database
79
+ database: testdb
80
+ username: postgres
81
+ password: readyset
82
+ adapter: postgresql
83
+ port: 5432
84
+ readyset:
85
+ # This is the connection information for ReadySet
86
+ database: testdb
87
+ username: postgres
88
+ password: readyset
89
+ adapter: readyset
90
+ host: "127.0.0.1"
91
+ port: 5433
92
+ # This setting tells Rails that there's no need to run migrations or other database
93
+ # administration tasks against ReadySet
94
+ database_tasks: false
95
+ ```
96
+ You can verify that ReadySet is up and your application is connected by
97
+ running `rails readyset:status`:
98
+ ```sh
99
+ $ rails readyset:status
100
+ +----------------------------+------------------------+
101
+ | Database Connection | Connected |
102
+ | Connection Count | 1 |
103
+ | Snapshot Status | Completed |
104
+ | Maximum Replication Offset | (0/6DBBD78, 0/6DBBFF0) |
105
+ | Minimum Replication Offset | (0/6DBBD78, 0/6DBBFF0) |
106
+ | Last started Controller | 2024-01-17 18:49:02 |
107
+ | Last completed snapshot | 2024-01-19 15:18:02 |
108
+ | Last started replication | 2024-01-19 15:18:02 |
109
+ +----------------------------+------------------------+
110
+ ```
111
+ You can also view the tables that ReadySet knows about and their statuses by
112
+ running `rails readyset:tables`:
113
+ ```sh
114
+ $ rails readyset:tables
115
+ +---------------------------------+-------------+-------------+
116
+ | table | status | description |
117
+ +---------------------------------+-------------+-------------+
118
+ | "public"."posts" | Snapshotted | |
119
+ +---------------------------------+-------------+-------------+
120
+ ```
121
+ 4. Run `Readyset.configure` wherever you configure other gems in your
122
+ application, and set any desired configuration options:
123
+ ```ruby
124
+ Readyset.configure do |config|
125
+ # Set your config options here
126
+ end
127
+ ```
128
+ The list of available configuration options can be found
129
+ [here](#configuration-options).
130
+ 5. FOR RAILS 7 USERS: Enable Rails's query log tags features by setting
131
+ `config.active_record.query_log_tags_enabled = true` wherever you configure
132
+ your ActiveRecord settings. This will append information to your
133
+ ActiveRecord query logs that tells you where the given query was routed (e.g.
134
+ to ReadySet or to your primary database)
135
+
136
+ ## Quickstart
137
+
138
+ 1. Follow the instructions above to set up ReadySet and install the gem
139
+ 2. Route a query to ReadySet in your application like so:
140
+ ```ruby
141
+ Readyset.route do
142
+ Post.where(user_id: user_id)
143
+ end
144
+ ```
145
+ 3. Start up your application and drive traffic through the part of your
146
+ application that invokes the query you routed in the previous step
147
+ 4. If you are running Rails 7 and enabled query log tags as explained in step 5
148
+ [above](#installing), you should see an annotation next to the query in your
149
+ application logs that denotes where the query was routed. If the
150
+ `destination` tag has the value `readyset`, the query was routed to
151
+ ReadySet.
152
+
153
+ If you are running Rails 6, you can validate that the query was routed to
154
+ ReadySet by running `rails readyset:proxied_queries`. A "proxied" query is
155
+ one that was served by ReadySet but was proxied to your primary database,
156
+ since a cache for the query does not yet exist:
157
+ ```sh
158
+ $ rails readyset:proxied_queries
159
+ +--------------------+-------------------------------------------------------+-------------+-------+
160
+ | id | text | supported | count |
161
+ +--------------------+-------------------------------------------------------+-------------+-------+
162
+ | q_281c5f9b8e4013bb | SELECT | yes | 1 |
163
+ | | * | | |
164
+ | | FROM | | |
165
+ | | "posts" | | |
166
+ | | WHERE | | |
167
+ | | ("user_id" = $1) | | |
168
+ +--------------------+-------------------------------------------------------+-------------+-------+
169
+ ```
170
+ 5. Create a cache for the query by running
171
+ `rails readyset:proxied_queries:cache_all_supported`. This will create caches for
172
+ all of the queries proxied by ReadySet that are supported to be cached. You
173
+ can verify that the expected caches were created by running
174
+ `rails readyset:caches`:
175
+ ```sh
176
+ $ rails readyset:caches
177
+ +--------------------+--------------------+-------------------------------------+--------+-------+
178
+ | id | name | text | always | count |
179
+ +--------------------+--------------------+-------------------------------------+--------+-------+
180
+ | q_281c5f9b8e4013bb | q_281c5f9b8e4013bb | SELECT | false | 0 |
181
+ | | | "public"."posts"."user_id" | | |
182
+ | | | FROM | | |
183
+ | | | "public"."posts" | | |
184
+ | | | WHERE | | |
185
+ | | | ("public"."posts"."user_id" = $1) | | |
186
+ +--------------------+--------------------+-------------------------------------+--------+-------+
187
+ ```
188
+ 6. Drive traffic through the part of your application that invokes your cached
189
+ query. The first invocation of the query will be a cache miss, but the
190
+ second will be served from the cache. You can verify that the cache was
191
+ successfully used by looking at the `count` column in the output of
192
+ `rails readyset:caches`:
193
+ ```sh
194
+ $ rails readyset:caches
195
+ +--------------------+--------------------+-------------------------------------+--------+-------+
196
+ | id | name | text | always | count |
197
+ +--------------------+--------------------+-------------------------------------+--------+-------+
198
+ | q_281c5f9b8e4013bb | q_281c5f9b8e4013bb | SELECT | false | 1 |
199
+ | | | "public"."posts"."user_id" | | |
200
+ | | | FROM | | |
201
+ | | | "public"."posts" | | |
202
+ | | | WHERE | | |
203
+ | | | ("public"."posts"."user_id" = $1) | | |
204
+ +--------------------+--------------------+-------------------------------------+--------+-------+
205
+ ```
206
+
207
+ ## Usage
208
+
209
+ ### Getting Started with Caching
210
+
211
+ Queries in arbitrary code blocks can be routed to ReadySet using the
212
+ `Readyset.route` method like so:
213
+
214
+ ```ruby
215
+ Readyset.route do
216
+ Post.where(user_id: user_id)
217
+ end
218
+ ```
219
+
220
+ Any queries invoked in the given block will be routed to the ReadySet instance
221
+ configured in your `config/database.yml` file; however, until a cache is created
222
+ for a particular query, invocations of that query against ReadySet will be proxied
223
+ to your database. To create a cache for a specific query, you have a few options:
224
+
225
+ - Invoke `.create_readyset_cache` directly on an ActiveRecord query in the
226
+ Rails console:
227
+ ```ruby
228
+ Post.where(user_id: 1).create_readyset_cache!
229
+ ```
230
+ - Create caches for all of the queries supported by ReadySet that ReadySet has
231
+ seen and proxied to your database since it last started up using the provided
232
+ Rake task:
233
+ ```sh
234
+ rails readyset:proxied_queries:cache_all_supported
235
+ ```
236
+ **Note:** If you route a query to ReadySet, decide you no longer want to cache
237
+ that query, and stop routing that query to ReadySet, that query will still
238
+ exist in ReadySet's list of queries that it has proxied to your database.
239
+ This means that running the above Rake task will still create a cache for that
240
+ query **even though it is no longer annotated to be routed to ReadySet in your
241
+ application code**. The list of queries ReadySet has proxied can be cleared by
242
+ restarting ReadySet or by running `rails readyset:proxied_queries:drop_all`.
243
+ - View the list of queries that ReadySet has proxied by running the following
244
+ in a Rails console:
245
+ ```ruby
246
+ Readyset::Query::ProxiedQuery.all
247
+ ```
248
+ You can invoke `#cache!` on the queries in this list for which you'd like to
249
+ create caches on ReadySet.
250
+ - View the list of queries that ReadySet has proxied *and* that are supported
251
+ by ReadySet to be cached by running the following:
252
+ ```sh
253
+ rails readyset:proxied_queries:supported
254
+ ```
255
+ Pick a query from the list that you'd like to cache, and pass the ID to the
256
+ `rails readyset:create_cache` command like so:
257
+ ```sh
258
+ rails 'readyset:create_cache[your_query_id]'
259
+ ```
260
+
261
+ Once a cache has been created for a particular query, it will persist on
262
+ ReadySet across restarts (although any in-memory cached data will be lost when
263
+ ReadySet goes down). You can view the list of existing caches using the provided
264
+ Rake task:
265
+ ```sh
266
+ rails readyset:caches
267
+ ```
268
+ To drop a given cache in the list printed by the above command, you can pass the
269
+ name of the cache to the `readyset:caches:drop` Rake task like so:
270
+ ```sh
271
+ rails 'readyset:caches:drop[my_cache]'
272
+ ```
273
+ You can also view the list of existing caches in an interactive form via the
274
+ Rails console:
275
+ ```ruby
276
+ Readyset::Query::CachedQuery.all
277
+ ```
278
+ You can invoke `#drop!` on any of the caches in this list to remove the cache
279
+ from ReadySet.
280
+
281
+ #### Query Routing in Controllers
282
+
283
+ The gem includes an extension to `ActionController` that allows you to route to
284
+ ReadySet all of the queries that occur within the context of a given controller
285
+ action:
286
+ ```ruby
287
+ class PostsController < ActionController
288
+ route_to_readyset only: :show
289
+
290
+ def show
291
+ @post = Post.where(id: params[:id])
292
+ end
293
+ end
294
+ ```
295
+ `route_to_readyset` takes the same parameters as Rails's
296
+ [`around_filters`](https://guides.rubyonrails.org/action_controller_overview.html#after-filters-and-around-filters).
297
+
298
+ #### Query Routing in Models
299
+
300
+ The gem also includes an extension that allows you to define queries in your
301
+ model that will be routed to ReadySet:
302
+ ```ruby
303
+ class Post < ApplicationRecord
304
+ readyset_query :posts_for_user, ->(user_id) { where(user_id: user_id) }
305
+ end
306
+ ```
307
+ The above example will define a `.posts_for_user` class method on the `Post`
308
+ model that invokes the query `Post.where(user_id: user_id)` against ReadySet.
309
+ **Note that other invocations of this query outside of the context of the
310
+ `.posts_for_user` method will not be routed to ReadySet.**
311
+
312
+ This feature allows you to specify which queries should be routed to ReadySet
313
+ in a centralized location and prevents the need to use `Readyset.route`
314
+ everywhere a cached query is invoked.
315
+
316
+ ### Cache Migrations
317
+
318
+ Once you have a set of caches you are happy with in your development
319
+ environment, you'll need a way to easily reproduce the same set of caches in
320
+ other environments (e.g. staging, production, etc.). To facilitate this, the
321
+ gem includes a "migration" feature, that allows you to dump the current set of
322
+ caches to a "migration file" and re-create these caches using the same
323
+ migration file.
324
+
325
+ The following Rake task dumps the current set of caches to the
326
+ `db/readyset_caches.rb` file:
327
+ ```sh
328
+ rails readyset:caches:dump
329
+ ```
330
+ This file should be checked into version control with your application code. To
331
+ update a ReadySet instance so that its set of caches matches the caches in your
332
+ migration file:
333
+ ```sh
334
+ rails readyset:caches:migrate
335
+ ```
336
+ This command 1) drops any caches that exist on ReadySet that are not present in
337
+ the migration file and 2) creates any caches that are present in the migration
338
+ file that do not exist on ReadySet. To run the command for a particular Rails
339
+ environment, you can set the `RAILS_ENV` environment variable.
340
+
341
+ ### Automatic Failover
342
+
343
+ To handle situations where ReadySet is unreachable for any reason, the gem
344
+ includes an automatic failover feature. The gem tracks the number of ReadySet
345
+ connection failures over a configurable window of time, and if the number of
346
+ errors exceeds the configured threshold, any queries previously being routed to
347
+ ReadySet will be routed to the primary database. A background task is started up
348
+ that periodically attempts to establish a connection to ReadySet and check its
349
+ status. When the task confirms that ReadySet is available again, it allows
350
+ queries to be routed to ReadySet again.
351
+
352
+ This feature is disabled by default. To enable it, set the
353
+ `config.failover.enabled` configuration option to `true`. You can read about the
354
+ other available configuration options [here](#configuration-options).
355
+
356
+ ## Configuration Options
357
+
358
+ The gem's configuration options can be set by passing a block to
359
+ `Readyset.configure` and setting options on the yielded
360
+ `Readyset::Configuration` object. The available options are documented below.
361
+ The values below are the default values for each of the options.
362
+
363
+ ```ruby
364
+ Readyset.configure do |config|
365
+ # Whether the gem's automatic failover feature should be enabled.
366
+ config.failover.enabled = false
367
+ # Sets the interval upon which the background task will check
368
+ # ReadySet's availability after failover has occurred.
369
+ config.failover.healthcheck_interval = 5.seconds
370
+ # Sets the time window over which connection errors are counted
371
+ # when determining whether failover should occur.
372
+ config.failover.error_window_period = 1.minute
373
+ # Sets the number of errors that must occur within the configured
374
+ # error window period in order for failover to be triggered.
375
+ config.failover.error_window_size = 10
376
+ # The file in which cache migrations should be stored.
377
+ config.migration_path = File.join(Rails.root, 'db/readyset_caches.rb')
378
+ end
379
+ ```
380
+
381
+ ## License
382
+
383
+ [MIT License](LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i(spec rubocop)
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
@@ -0,0 +1,54 @@
1
+ services:
2
+ readyset:
3
+ image: public.ecr.aws/readyset/readyset:latest-stable
4
+ platform: linux/amd64
5
+ ports:
6
+ # The ReadySet Adapter listen port, i.e. what your application / SQL shell connects to
7
+ - "5433:5433"
8
+ # ReadySet Prometheus metrics available at http://localhost:6034/metrics
9
+ # e.g. curl -X GET http://localhost:6034/metrics
10
+ - "6034:6034"
11
+ environment:
12
+ DEPLOYMENT_ENV: readyset_rails_test
13
+ DB_DIR: /state
14
+ QUERY_CACHING: explicit
15
+ STANDALONE: 'true'
16
+ DEPLOYMENT: docker_compose_deployment
17
+ LISTEN_ADDRESS: 0.0.0.0:5433
18
+ UPSTREAM_DB_URL: postgresql://postgres:readyset@postgres/testdb
19
+ CONTROLLER_ADDRESS: 0.0.0.0
20
+ volumes:
21
+ - "readyset:/state"
22
+ healthcheck:
23
+ test: [ "CMD", "curl", "--fail", "127.0.0.1:6034/health" ]
24
+ interval: 2s
25
+ timeout: 1s
26
+ retries: 5
27
+ start_period: 5s
28
+ depends_on:
29
+ postgres:
30
+ condition: service_healthy
31
+ postgres:
32
+ image: postgres:14
33
+ environment:
34
+ - POSTGRES_PASSWORD=readyset
35
+ - POSTGRES_DB=testdb
36
+ ports:
37
+ # The ReadySet Adapter listen port, i.e. what your application / SQL shell connects to
38
+ - "5432:5432"
39
+ expose:
40
+ - 5432
41
+ command:
42
+ - "postgres"
43
+ - "-c"
44
+ - "wal_level=logical"
45
+ healthcheck:
46
+ test: ["CMD", "pg_isready", "-U", "postgres"]
47
+ interval: 5s
48
+ timeout: 5s
49
+ retries: 12
50
+ volumes:
51
+ - postgres:/var/lib/postgresql/data
52
+ volumes:
53
+ postgres: ~
54
+ readyset: ~
@@ -0,0 +1,52 @@
1
+ require 'active_record'
2
+ require 'active_record/connection_adapters/abstract_adapter'
3
+ require 'active_record/connection_adapters/postgresql_adapter'
4
+ require 'readyset/error'
5
+
6
+ module ActiveRecord
7
+ module ConnectionAdapters
8
+ # The ReadySet adapter is a proxy object that delegates all its methods to an inner
9
+ # PostgreSQLAdapter instance.
10
+ class ReadysetAdapter
11
+ ADAPTER_NAME = 'Readyset'.freeze
12
+
13
+ # Finds the root cause of the given error and includes the Readyset::Error module in that
14
+ # error's singleton class if the root cause was a `PG::Error`. This allows us to invoke
15
+ # `#is_a?` on the error to determine if the error came from a connection to ReadySet.
16
+ #
17
+ # @param e [Exception] the error whose cause should be annotated
18
+ # @return [void]
19
+ def self.annotate_error(e)
20
+ if e.cause
21
+ annotate_error(e.cause)
22
+ else
23
+ if e.is_a?(::PG::Error)
24
+ e.singleton_class.instance_eval do
25
+ include ::Readyset::Error
26
+ end
27
+ end
28
+ end
29
+
30
+ nil
31
+ end
32
+
33
+ def self.method_missing(...)
34
+ PostgreSQLAdapter.send(...)
35
+ rescue => e
36
+ annotate_error(e)
37
+ raise e
38
+ end
39
+
40
+ def initialize(pg_conn)
41
+ @inner = pg_conn
42
+ end
43
+
44
+ def method_missing(...)
45
+ @inner.send(...)
46
+ rescue => e
47
+ self.class.annotate_error(e)
48
+ raise e
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ # The methods in these modules are required for Rails to recognize our custom adapter
5
+ module ReadysetConnectionHandling
6
+ def readyset_adapter_class
7
+ ConnectionAdapters::ReadysetAdapter
8
+ end
9
+
10
+ def readyset_connection(config) # :nodoc:
11
+ pg_conn = postgresql_connection(config)
12
+ readyset_adapter_class.new(pg_conn)
13
+ rescue => e
14
+ readyset_adapter_class.annotate_error(e)
15
+ raise e
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module Readyset
2
+ # Defines the DSL used in the gem's "migration" files. The DSL should be used by inheriting
3
+ # from this class and invoking the `.cache` class method to define new caches.
4
+ class Caches
5
+ class << self
6
+ attr_reader :caches
7
+ end
8
+
9
+ def self.cache(always: false)
10
+ @caches ||= Set.new
11
+
12
+ query = yield
13
+
14
+ @caches << Query::CachedQuery.new(
15
+ text: query.strip,
16
+ always: always,
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # lib/readyset/configuration.rb
2
+ require 'active_record'
3
+
4
+ module Readyset
5
+ class Configuration
6
+ attr_accessor :migration_path, :shard
7
+
8
+ def initialize
9
+ @migration_path = File.join(Rails.root, 'db/readyset_caches.rb')
10
+ @shard = :readyset
11
+ end
12
+
13
+ def failover
14
+ if @failover
15
+ @failover
16
+ else
17
+ inner = ActiveSupport::OrderedOptions.new
18
+ inner.enabled = false
19
+ inner.healthcheck_interval = 5.seconds
20
+ inner.error_window_period = 1.minute
21
+ inner.error_window_size = 10
22
+ @failover = inner
23
+ end
24
+ end
25
+
26
+ def hostname
27
+ ActiveRecord::Base.configurations.configs_for(name: shard.to_s).configuration_hash[:host]
28
+ end
29
+ end
30
+ end