diff --git a/.gitignore b/.gitignore index 6d26d9b..8404fac 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ # rspec failure tracking .rspec_status Gemfile.lock +spec/internal/db/readyset_caches.rb spec/internal/tmp/ diff --git a/README.md b/README.md index c88d7e5..b763e70 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: diff --git a/lib/tasks/readyset.rake b/lib/tasks/readyset.rake index ca22eb3..b1a896f 100644 --- a/lib/tasks/readyset.rake +++ b/lib/tasks/readyset.rake @@ -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 + Rails.logger.error '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 + Rails.logger.error '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) + + Rails.logger.info table.to_s + 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'.freeze) + 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) + + Rails.logger.info table.to_s + 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) + + Rails.logger.info table.to_s + 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 + Rails.logger.error '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 @@ -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 @@ -65,8 +137,26 @@ namespace :readyset do end end else - puts 'Nothing to do' + Rails.logger.info 'Nothing to do' end end end + + desc 'Prints status information about ReadySet' + task status: :environment do + rows = Readyset.raw_query('SHOW READYSET STATUS'.freeze). + map { |result| [result['name'], result['value']] } + table = Terminal::Table.new(rows: rows) + + Rails.logger.info table.to_s + end + + desc 'Prints information about the tables known to ReadySet' + task tables: :environment do + rows = Readyset.raw_query('SHOW READYSET TABLES'.freeze). + map { |result| [result['table'], result['status'], result['description']] } + table = Terminal::Table.new(headings: [:table, :status, :description], rows: rows) + + Rails.logger.info table.to_s + end end diff --git a/readyset.gemspec b/readyset.gemspec index 7dfc27e..ed00a44 100644 --- a/readyset.gemspec +++ b/readyset.gemspec @@ -38,6 +38,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'concurrent-ruby', '~> 1.2' 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' diff --git a/spec/rake_spec.rb b/spec/rake_spec.rb index e080994..eec802d 100644 --- a/spec/rake_spec.rb +++ b/spec/rake_spec.rb @@ -10,17 +10,124 @@ 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 + expected_message = 'A query ID must be passed to this task' + allow(Rails.logger).to receive(:error).with(expected_message) + + Rake::Task['readyset:create_cache'].execute + + expect(Rails.logger).to have_received(:error).with(expected_message) + 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 + expected_message = 'A query ID must be passed to this task' + allow(Rails.logger).to receive(:error).with(expected_message) + + Rake::Task['readyset:create_cache'].execute + + expect(Rails.logger).to have_received(:error).with(expected_message) + 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.chomp + +--------------------+--------------------+---------------------------------+--------+-------+ + | id | name | text | always | count | + +--------------------+--------------------+---------------------------------+--------+-------+ + | q_4f3fb9ad8f73bc0c | q_4f3fb9ad8f73bc0c | SELECT | false | 0 | + | | | "public"."cats"."breed" | | | + | | | FROM | | | + | | | "public"."cats" | | | + | | | WHERE | | | + | | | ("public"."cats"."name" = $1) | | | + +--------------------+--------------------+---------------------------------+--------+-------+ + TABLE + allow(Rails.logger).to receive(:info).with(expected_message) + + Rake::Task['readyset:caches'].execute + + expect(Rails.logger).to have_received(:info).with(expected_message) + 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 + expected_message = 'A cache name must be passed to this task' + allow(Rails.logger).to receive(:error).with(expected_message) + + Rake::Task['readyset:caches:drop'].execute + + expect(Rails.logger).to have_received(:error).with(expected_message) + 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) @@ -113,5 +220,124 @@ 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.chomp + \\+--------------------\\+------------------------\\+-----------\\+-------\\+ + \\| id \\| text \\| supported \\| count \\| + \\+--------------------\\+------------------------\\+-----------\\+-------\\+ + \\| q_4f3fb9ad8f73bc0c \\| SELECT \\| pending \\| \\d+[ ]*\\| + \\| \\| "cats"\\."breed" \\| \\| [ ]*\\| + \\| \\| FROM \\| \\| [ ]*\\| + \\| \\| "cats" \\| \\| [ ]*\\| + \\| \\| WHERE \\| \\| [ ]*\\| + \\| \\| \\("cats"\\."name" = \\$1\\) \\| \\| [ ]*\\| + \\+--------------------\\+------------------------\\+-----------\\+-------\\+ + TABLE + allow(Rails.logger).to receive(:info).with(expected_message) + + Rake::Task['readyset:proxied_queries'].execute + + expect(Rails.logger).to have_received(:info).with(expected_message) + 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.chomp + \\+--------------------\\+------------------------\\+-------\\+ + \\| id \\| text \\| count \\| + \\+--------------------\\+------------------------\\+-------\\+ + \\| q_4f3fb9ad8f73bc0c \\| SELECT \\| \\d+[ ]*\\| + \\| \\| "cats"\\."breed" \\| [ ]*\\| + \\| \\| FROM \\| [ ]*\\| + \\| \\| "cats" \\| [ ]*\\| + \\| \\| WHERE \\| [ ]*\\| + \\| \\| \\("cats"\\."name" = \\$1\\) \\| [ ]*\\| + \\+--------------------\\+------------------------\\+-------\\+ + TABLE + allow(Rails.logger).to receive(:info).with(expected_message) + + Rake::Task['readyset:proxied_queries:supported'].execute + + expect(Rails.logger).to have_received(:info).with(expected_message) + end + end + end + + describe 'status' do + it "prints a table that shows ReadySet's status" do + expected_message = Regexp.new <<~TABLE.chomp + \\+----------------------------\\+------------------------\\+ + \\| 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 + allow(Rails.logger).to receive(:info).with(expected_message) + + Rake::Task['readyset:status'].execute + + expect(Rails.logger).to have_received(:info).with(expected_message) + end + end + + describe 'tables' do + it 'prints a table that shows the tables known to ReadySet' do + expected_message = Regexp.new <<~TABLE.chomp + \\+---------------------------------\\+-------------\\+-------------\\+ + \\| table \\| status \\| description \\| + \\+---------------------------------\\+-------------\\+-------------\\+ + (\\| "public"\\."[\\w]*"[ ]*\\| Snapshotted \\| \\|\n?)* + \\+---------------------------------\\+-------------\\+-------------\\+ + TABLE + allow(Rails.logger).to receive(:info).with(expected_message) + + Rake::Task['readyset:tables'].execute + + expect(Rails.logger).to have_received(:info).with(expected_message) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d08dc98..22e1e51 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,9 +10,7 @@ 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'