Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rake: Add administrative tasks #73

Merged
merged 12 commits into from
Jan 26, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
# rspec failure tracking
.rspec_status
Gemfile.lock
spec/internal/db/readyset_caches.rb
spec/internal/tmp/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ to your database. To create a cache for a specific query, you have a few options
Pick a query from the list that you'd like to cache, and pass the ID to the
`rails readyset:create_cache` command like so:
```sh
rails readyset:create_cache[your_query_id]
rails 'readyset:create_cache[your_query_id]'
```

Once a cache has been created for a particular query, it will persist on
Expand All @@ -268,7 +268,7 @@ rails readyset:caches
To drop a given cache in the list printed by the above command, you can pass the
name of the cache to the `readyset:caches:drop` Rake task like so:
```sh
rails readyset:caches:drop[my_cache]
rails 'readyset:caches:drop[my_cache]'
```
You can also view the list of existing caches in an interactive form via the
Rails console:
Expand Down
98 changes: 94 additions & 4 deletions lib/tasks/readyset.rake
Original file line number Diff line number Diff line change
@@ -1,13 +1,87 @@
require 'colorize'
require 'erb'
require 'progressbar'
require 'terminal-table'

namespace :readyset do
desc 'Creates a cache from the given query ID'
task :create_cache, [:id] => :environment do |_, args|
if args.key?(:id)
Readyset.create_cache!(id: args[:id])
else
puts 'A query ID must be passed to this task'
end
end

desc 'Creates a cache from the given query ID whose queries will never fall back to the ' \
'primary database'
task :create_cache_always, [:id] => :environment do |_, args|
if args.key?(:id)
Readyset.create_cache!(id: args[:id], always: true)
else
puts 'A query ID must be passed to this task'
end
end

desc 'Prints a list of all the queries that ReadySet has proxied'
task proxied_queries: :environment do
rows = Readyset::Query::ProxiedQuery.all.map do |q|
[q.id, q.text, q.supported, q.count]
end
table = Terminal::Table.new(headings: [:id, :text, :supported, :count], rows: rows)

puts table
end

namespace :proxied_queries do
desc 'Creates caches for all of the supported queries on ReadySet'
task cache_all_supported: :environment do
Readyset::Query::ProxiedQuery.cache_all_supported!
end

desc 'Clears the list of proxied queries on ReadySet'
task drop_all: :environment do
Readyset.raw_query('DROP ALL PROXIED QUERIES')
end

desc 'Prints a list of all the queries that ReadySet has proxied that can be cached'
task supported: :environment do
rows = Readyset::Query::ProxiedQuery.all.
select { |query| query.supported == :yes }.
map { |q| [q.id, q.text, q.count] }
table = Terminal::Table.new(headings: [:id, :text, :count], rows: rows)

puts table
end
end

desc 'Prints a list of all the cached queries on ReadySet'
task caches: :environment do
rows = Readyset::Query::CachedQuery.all.map do |q|
[q.id, q.name, q.text, q.always, q.count]
end
table = Terminal::Table.new(headings: [:id, :name, :text, :always, :count], rows: rows)

puts table
end

namespace :caches do
desc 'Drops the cache with the given name'
task :drop, [:name] => :environment do |_, args|
if args.key?(:name)
Readyset.drop_cache!(args[:name])
else
puts 'A cache name must be passed to this task'
end
end

desc 'Drops all the caches on ReadySet'
task drop_all: :environment do
Readyset::Query::CachedQuery.drop_all!
end

desc 'Dumps the set of caches that currently exist on ReadySet to a file'
task dump: :environment do
Rails.application.eager_load!

template = File.read(File.join(File.dirname(__FILE__), '../templates/caches.rb.tt'))

queries = Readyset::Query::CachedQuery.all
Expand All @@ -20,8 +94,6 @@ namespace :readyset do
desc 'Synchronizes the caches on ReadySet such that the caches on ReadySet match those ' \
'listed in db/readyset_caches.rb'
task migrate: :environment do
Rails.application.eager_load!

file = Readyset.configuration.migration_path

# We load the definition of the `Readyset::Caches` subclass in the context of a
Expand Down Expand Up @@ -69,4 +141,22 @@ namespace :readyset do
end
end
end

desc 'Prints status information about ReadySet'
task status: :environment do
rows = Readyset.raw_query('SHOW READYSET STATUS').
map { |result| [result['name'], result['value']] }
table = Terminal::Table.new(rows: rows)

puts table
end

desc 'Prints information about the tables known to ReadySet'
task tables: :environment do
rows = Readyset.raw_query('SHOW READYSET TABLES').
map { |result| [result['table'], result['status'], result['description']] }
table = Terminal::Table.new(headings: [:table, :status, :description], rows: rows)

puts table
end
end
1 change: 1 addition & 0 deletions readyset.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'colorize', '~> 1.1'
spec.add_dependency 'progressbar', '~> 1.13'
spec.add_dependency 'rake', '~> 13.0'
spec.add_dependency 'terminal-table', '~> 3.0'

spec.add_development_dependency 'combustion', '~> 1.3'
spec.add_development_dependency 'factory_bot', '~> 6.4'
Expand Down
205 changes: 202 additions & 3 deletions spec/rake_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,108 @@
end

describe 'readyset' do
describe 'create_cache' do
context 'when given a query ID as an argument' do
it 'creates a cache from the given query' do
query = build_and_execute_proxied_query(:proxied_query)
build_and_execute_proxied_query(:proxied_query_2)

Rake::Task['readyset:create_cache'].execute(Rake::TaskArguments.new([:id], [query.id]))

caches = Readyset::Query::CachedQuery.all
expect(caches).to eq([build(:cached_query)])
end
end

context 'when given no arguments' do
it 'prints an error message' do
expect { Rake::Task['readyset:create_cache'].execute }.
to output("A query ID must be passed to this task\n").to_stdout
end
end
end

describe 'create_cache_always' do
context 'when given a query ID as an argument' do
it 'creates a cache from the given query with `always` set to true' do
query = build_and_execute_proxied_query(:proxied_query)
build_and_execute_proxied_query(:proxied_query_2)

Rake::Task['readyset:create_cache_always'].
execute(Rake::TaskArguments.new([:id], [query.id]))

caches = Readyset::Query::CachedQuery.all
expect(caches).to eq([build(:cached_query, always: true)])
end
end

context 'when given no arguments' do
it 'prints an error message' do
expect { Rake::Task['readyset:create_cache_always'].execute }.
to output("A query ID must be passed to this task\n").to_stdout
end
end
end

describe 'caches' do
it 'prints a table with the caches that currently exist on ReadySet' do
build_and_create_cache(:cached_query)

expected_message = <<~TABLE
+--------------------+--------------------+---------------------------------+--------+-------+
| id | name | text | always | count |
+--------------------+--------------------+---------------------------------+--------+-------+
| q_4f3fb9ad8f73bc0c | q_4f3fb9ad8f73bc0c | SELECT | false | 0 |
| | | "public"."cats"."breed" | | |
| | | FROM | | |
| | | "public"."cats" | | |
| | | WHERE | | |
| | | ("public"."cats"."name" = $1) | | |
+--------------------+--------------------+---------------------------------+--------+-------+
TABLE
expect { Rake::Task['readyset:caches'].execute }.to output(expected_message).to_stdout
end

describe 'drop' do
context 'when given a cache name as an argument' do
it 'removes the cache with the given name' do
cache = build_and_create_cache(:cached_query)

Rake::Task['readyset:caches:drop'].
execute(Rake::TaskArguments.new([:name], [cache.name]))

caches = Readyset::Query::CachedQuery.all
expect(caches.size).to eq(0)
end
end

context 'when given no arguments' do
it 'prints an error message' do
expect { Rake::Task['readyset:caches:drop'].execute }.
to output("A cache name must be passed to this task\n").to_stdout
end
end
end

describe 'drop_all' do
it 'removes all the caches on ReadySet' do
build_and_create_cache(:cached_query)
build_and_create_cache(:cached_query_2)

Rake::Task['readyset:caches:drop_all'].execute

caches = Readyset::Query::CachedQuery.all
expect(caches.size).to eq(0)
end
end

describe 'dump' do
it 'dumps the current set of caches to a migration file' do
# Setup
cache1 = build_and_create_cache(:cached_query, count: nil, id: nil, name: nil)
cache2 = build_and_create_cache(:cached_query_2, count: nil, id: nil, name: nil)

# Execute
Rake::Task['readyset:caches:dump'].execute

# Verify
load './spec/internal/db/readyset_caches.rb'
subclasses = Readyset::Caches.subclasses
expect(subclasses.size).to eq(1)
Expand Down Expand Up @@ -113,5 +204,113 @@
end
end
end

describe 'proxied_queries' do
it 'prints a table with the queries that ReadySet has proxied' do
build_and_execute_proxied_query(:proxied_query)

expected_message = Regexp.new <<~TABLE
\\+--------------------\\+------------------------\\+-----------\\+-------\\+
\\| id \\| text \\| supported \\| count \\|
\\+--------------------\\+------------------------\\+-----------\\+-------\\+
\\| q_4f3fb9ad8f73bc0c \\| SELECT \\| pending \\| \\d+[ ]*\\|
\\| \\| "cats"\\."breed" \\| \\| [ ]*\\|
\\| \\| FROM \\| \\| [ ]*\\|
\\| \\| "cats" \\| \\| [ ]*\\|
\\| \\| WHERE \\| \\| [ ]*\\|
\\| \\| \\("cats"\\."name" = \\$1\\) \\| \\| [ ]*\\|
\\+--------------------\\+------------------------\\+-----------\\+-------\\+
TABLE
expect { Rake::Task['readyset:proxied_queries'].execute }.to output(expected_message).
to_stdout
end

describe 'cache_all_supported' do
it 'creates caches for all queries proxied by ReadySet that are supported to be cached' do
build_and_execute_proxied_query(:proxied_query)
build_and_execute_proxied_query(:unsupported_proxied_query)

eventually do
Readyset::Query::ProxiedQuery.all.all? { |query| query.supported != :pending }
end

Rake::Task['readyset:proxied_queries:cache_all_supported'].execute

expect(Readyset::Query::CachedQuery.all).to eq([build(:cached_query)])
end
end

describe 'drop_all' do
it 'clears the list of proxied queries on ReadySet' do
build_and_execute_proxied_query(:proxied_query)
build_and_execute_proxied_query(:proxied_query_2)

Rake::Task['readyset:proxied_queries:drop_all'].execute

proxied = Readyset::Query::ProxiedQuery.all
expect(proxied).to be_empty
end
end

describe 'supported' do
it 'prints a table that shows only proxied queries supported by ReadySet' do
build_and_execute_proxied_query(:proxied_query)
build_and_execute_proxied_query(:unsupported_proxied_query)

eventually do
Readyset::Query::ProxiedQuery.all.all? { |query| query.supported != :pending }
end

expected_message = Regexp.new <<~TABLE
\\+--------------------\\+------------------------\\+-------\\+
\\| id \\| text \\| count \\|
\\+--------------------\\+------------------------\\+-------\\+
\\| q_4f3fb9ad8f73bc0c \\| SELECT \\| \\d+[ ]*\\|
\\| \\| "cats"\\."breed" \\| [ ]*\\|
\\| \\| FROM \\| [ ]*\\|
\\| \\| "cats" \\| [ ]*\\|
\\| \\| WHERE \\| [ ]*\\|
\\| \\| \\("cats"\\."name" = \\$1\\) \\| [ ]*\\|
\\+--------------------\\+------------------------\\+-------\\+
TABLE
expect { Rake::Task['readyset:proxied_queries:supported'].execute }.
to output(expected_message).to_stdout
end
end
end

describe 'status' do
it "prints a table that shows ReadySet's status" do
expected_message = Regexp.new <<~TABLE
\\+----------------------------\\+------------------------\\+
\\| Database Connection \\| Connected[ ]*\\|
\\| Connection Count \\| \\d+[ ]*\\|
\\| Snapshot Status \\| Completed[ ]*\\|
\\| Maximum Replication Offset \\| \\([0-9A-F]{1,8}\\/[0-9A-F]{1,8}, [0-9A-F]{1,8}\\/[0-9A-F]{1,8}\\) \\|
\\| Minimum Replication Offset \\| \\([0-9A-F]{1,8}\\/[0-9A-F]{1,8}, [0-9A-F]{1,8}\\/[0-9A-F]{1,8}\\) \\|
\\| Last started Controller \\| \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[ ]*\\|
\\| Last completed snapshot \\| \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[ ]*\\|
\\| Last started replication \\| \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[ ]*\\|
\\+----------------------------\\+------------------------\\+
TABLE
expect { Rake::Task['readyset:status'].execute }.to output(expected_message).
to_stdout
end
end

describe 'tables' do
it 'prints a table that shows the tables known to ReadySet' do
expected_message = Regexp.new <<~TABLE
\\+---------------------------------\\+-------------\\+-------------\\+
\\| table \\| status \\| description \\|
\\+---------------------------------\\+-------------\\+-------------\\+
(\\| "public"\\."[\\w]*"[ ]*\\| Snapshotted \\| \\|\n?)*
\\+---------------------------------\\+-------------\\+-------------\\+
TABLE

expect { Rake::Task['readyset:tables'].execute }.to output(expected_message).
to_stdout
end
end
end
end
4 changes: 1 addition & 3 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
require 'factory_bot'
require_relative 'shared_examples'

Combustion.initialize! :action_controller, :active_record, database_reset: false do
config.eager_load = true
end
Combustion.initialize! :action_controller, :active_record, database_reset: false

require 'readyset'

Expand Down
Loading