refactor and improve partition management

* move most of the logic into PartitionManager
 * use a setting for how many partitions to precreate/prune
 * enumerate existing tables for pruning, instead of assuming
   we can just roll off the last one
 * actually run the tests for partman from CI
 * simplify configuring partitioned migrations (just rely
   on the model to get the table name, instead of trying to
   guess the model from the table name, and sometimes having
   to provide both)

Change-Id: Ic8ac2b603a02f092b2f278d2b366b1cd9f942954
Reviewed-on: https://gerrit.instructure.com/53286
Tested-by: Jenkins
Reviewed-by: Ethan Vizitei <evizitei@instructure.com>
Reviewed-by: Brian Finney <bfinney@instructure.com>
Product-Review: Cody Cutrer <cody@instructure.com>
QA-Review: Cody Cutrer <cody@instructure.com>
This commit is contained in:
Cody Cutrer 2015-05-01 10:17:22 -06:00
parent 157ee9bfc6
commit be8b9e0772
14 changed files with 149 additions and 101 deletions

View File

@ -1,4 +1,4 @@
class Quizzes::QuizSubmissionEventPartitioner < ActiveRecord::Base
class Quizzes::QuizSubmissionEventPartitioner
cattr_accessor :logger
def self.process
@ -8,29 +8,9 @@ class Quizzes::QuizSubmissionEventPartitioner < ActiveRecord::Base
partman = CanvasPartman::PartitionManager.new(Quizzes::QuizSubmissionEvent)
[ Time.now.utc, 1.month.from_now(Time.now.utc) ].each do |date|
log "Looking for a table for partition #{date.strftime('%Y/%m')}..."
partman.ensure_partitions(Setting.get('quiz_events_partitions_precreate_months', 2).to_i)
if partman.partition_exists?(date)
log "\tPartition table exists, nothing to do. [OK]"
else
log "\tPartition table does not exist, creating..."
partition_table_name = partman.create_partition(date)
log "\tPartition table created: '#{partition_table_name}'. [OK]"
end
end
# on 5/1, we want to drop 10/1
# (keeping 11, 12, 1, 2, 3, and 4 - 6 months of data)
[ 7.months.ago(Time.now.utc.beginning_of_month) ].each do |date|
log "Looking for old partition table (#{date.strftime('%Y/%m')})..."
if partman.partition_exists?(date)
log "\tPartition table exists, dropping..."
partman.drop_partition(date)
log "\tPartition table dropped. [OK]"
end
end
partman.prune_partitions(Setting.get("quiz_events_partitions_keep_months", 6).to_i)
log 'Done. Bye!'
log '*' * 80

View File

@ -1,7 +1,6 @@
class AddIndicesToQuizSubmissionEvents < CanvasPartman::Migration
tag :predeploy
self.master_table = :quiz_submission_events
self.base_class = Quizzes::QuizSubmissionEvent
def up

View File

@ -7,7 +7,7 @@ platforms :ruby_19 do
gem 'debugger'
end
platforms :ruby_21 do
platforms :ruby_21, :ruby_22 do
gem 'byebug', require: false
end

View File

@ -4,19 +4,21 @@ require 'canvas_partman/dynamic_relation'
require 'canvas_partman/concerns/partitioned'
module CanvasPartman
# @property [String, "db/migrate"] migrations_paths
# Path (relative to Rails root) to where the partition migrations should be
# looked up.
mattr_accessor :migrations_path
class << self
# @property [String, "db/migrate"] migrations_paths
# Path (relative to Rails root) to where the partition migrations should be
# looked up.
attr_accessor :migrations_path
# @property [String, "partitions"] migrations_scope
# The filename "scope" that identifies partition migrations. This is a key
# that is separated from the name of the migration file and the "rb"
# extension by dots.
#
# Example: "partitions" => "20141215000000_add_something.partitions.rb"
mattr_accessor :migrations_scope
# @property [String, "partitions"] migrations_scope
# The filename "scope" that identifies partition migrations. This is a key
# that is separated from the name of the migration file and the "rb"
# extension by dots.
#
# Example: "partitions" => "20141215000000_add_something.partitions.rb"
attr_accessor :migrations_scope
end
self.migrations_path = 'db/migrate'
self.migrations_scope = 'partitions'
end
end

View File

@ -1,52 +1,43 @@
require 'active_record/migration'
class CanvasPartman::Migration < ActiveRecord::Migration
class << self
# @attr [String] master_table
# Name of the master table which partitions this migration will modify.
attr_accessor :master_table
module CanvasPartman
class Migration < ActiveRecord::Migration
class << self
# @attr [CanvasPartman::Partitioned] base_class
# The partitioned ActiveRecord::Base model _class_ we're modifying
attr_accessor :base_class
end
# @attr [CanvasPartman::Partitioned] base_class
# The partitioned ActiveRecord::Base model _class_ we're modifying (which is
# stored in the master_table we specified earlier.)
def with_each_partition(&block)
find_partition_tables.each(&block)
end
# Bind the migration to apply only on a certain partition table. Routines
# like #with_each_partition will yield only the specified table instead.
#
# If left unspecified, we try to infer the class from the master table name.
attr_accessor :base_class
# @param [String] table_name
# Name of the **existing** partition table.
def restrict_to_partition(table_name)
@partition_scope = table_name
yield
ensure
@partition_scope = nil
end
# :nodoc:
attr_reader :partition_table_matcher
def self.connection
(base_class || ActiveRecord::Base).connection
end
private
def partition_manager
@partition_manager ||= PartitionManager.new(self.class.base_class)
end
def find_partition_tables
return [ @partition_scope ] if @partition_scope
partition_manager.partition_tables
end
end
def with_each_partition(&block)
find_partition_tables.each(&block)
end
# Bind the migration to apply only on a certain partition table. Routines
# like #with_each_partition will yield only the specified table instead.
#
# @param [String] table_name
# Name of the **existing** partition table.
def restrict_to_partition(table_name)
@partition_scope = table_name
yield
ensure
@partition_scope = nil
end
def self.connection
(self.base_class || ActiveRecord::Base).connection
end
private
def self.master_table=(name)
@master_table = name.to_s.freeze
@partition_table_matcher = /^#{Regexp.escape(@master_table)}_/.freeze
end
def find_partition_tables
return [ @partition_scope ] if @partition_scope
self.class.connection.tables.grep(self.class.partition_table_matcher)
end
end
end

View File

@ -13,6 +13,36 @@ module CanvasPartman
@base_class = base_class
end
# Ensure the current partition, and n future partitions exist
#
# @param [Fixnum] advance
# The number of partitions to create in advance
def ensure_partitions(advance = 1)
current = Time.now.utc.send("beginning_of_#{base_class.partitioning_interval.to_s.singularize}")
(advance + 1).times do
unless partition_exists?(current)
create_partition(current)
end
current += 1.send(base_class.partitioning_interval)
end
end
# Prune old partitions
#
# @param [Fixnum] number_to_keep
# The number of partitions to keep (excluding the current partition)
def prune_partitions(number_to_keep = 6)
min_to_keep = Time.now.utc.send("beginning_of_#{base_class.partitioning_interval.to_s.singularize}")
# on 5/1, we want to drop 10/1
# (keeping 11, 12, 1, 2, 3, and 4 - 6 months of data)
min_to_keep -= number_to_keep.send(base_class.partitioning_interval)
partition_tables.each do |table|
partition_date = date_from_partition_name(table)
base_class.connection.drop_table(table) if partition_date < min_to_keep
end
end
# Create a new partition table.
#
# @param [Hash] options
@ -47,7 +77,7 @@ module CanvasPartman
INHERITS (#{master_table});
SQL
find_and_load_migrations(master_table).each do |migration|
find_and_load_migrations.each do |migration|
migration.restrict_to_partition(partition_table) do
migration.migrate(:up)
end
@ -57,6 +87,10 @@ module CanvasPartman
end
end
def partition_tables
base_class.connection.tables.grep(table_regex)
end
def partition_exists?(date_or_name)
table_name = if date_or_name.kind_of?(Time)
generate_name_for_partition(date_or_name)
@ -75,6 +109,15 @@ module CanvasPartman
protected
def table_regex
@table_regex ||= case base_class.partitioning_interval
when :months
/^#{Regexp.escape(base_class.table_name)}_(?<year>\d{4,})_(?<month>\d{1,2})$/.freeze
when :years
/^#{Regexp.escape(base_class.table_name)}_(?<year>\d{4,})$/.freeze
end
end
def generate_name_for_partition(date)
date_attr = Arel::Attributes::Attribute.new(nil, base_class.partitioning_field)
@ -84,6 +127,12 @@ module CanvasPartman
base_class.infer_partition_table_name(attributes)
end
def date_from_partition_name(name)
match = table_regex.match(name)
return nil unless match
Time.utc(*match[1..-1])
end
def generate_date_constraint_range(date)
case base_class.partitioning_interval
when :months
@ -104,14 +153,14 @@ module CanvasPartman
end
end
def find_and_load_migrations(master_table)
def find_and_load_migrations
ActiveRecord::Migrator.migrations(CanvasPartman.migrations_path).reduce([]) do |migrations, proxy|
if proxy.scope == CanvasPartman.migrations_scope
require(File.expand_path(proxy.filename))
migration_klass = proxy.name.constantize
if migration_klass.master_table == master_table
if migration_klass.base_class == base_class
migrations << migration_klass.new
end
end

View File

@ -8,15 +8,15 @@ class PartitionMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
source_root File.expand_path("../templates", __FILE__)
remove_argument :attributes
argument :master_table, type: :string, required: false,
desc: 'Name of the master table whose partitions will be modified.'
argument :model, type: :string, required: false,
desc: 'Name of the model whose partitions will be modified.'
def create_migration_file
unless file_name =~ /^[_a-z0-9]+$/
raise ActiveRecord::IllegalMigrationNameError.new(file_name)
end
migration_template 'migration.rb',
migration_template 'migration.rb.erb',
"db/migrate/#{file_name}.#{CanvasPartman.migrations_scope}.rb"
end
@ -25,4 +25,4 @@ class PartitionMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
def migration_class_name
name.camelize
end
end
end

View File

@ -1,10 +1,5 @@
class <%= migration_class_name %> < CanvasPartman::Migration
self.master_table = '<%= master_table || "name_of_master_table" %>'
# If the base class can not be infered from the master table name because, for
# example, it is namespaced, you may explicitly specify it here:
#
# self.base_class = Quizzes::QuizSubmissionEvent
self.base_class = '<%= model || "NameOfModel" %>'
def self.up
with_each_partition do |partition|

View File

@ -50,4 +50,37 @@ describe CanvasPartman::PartitionManager do
end
end
end
describe "#ensure_partitions" do
it "should create the proper number of partitions" do
expect(subject).to receive(:partition_exists?).at_least(:once).and_return(false)
expect(Time).to receive(:now).and_return(Time.utc(2015, 05, 02))
expect(subject).to receive(:create_partition).with(Time.utc(2015, 05, 01))
expect(subject).to receive(:create_partition).with(Time.utc(2015, 06, 01))
subject.ensure_partitions(1)
end
end
describe "#prune_partitions" do
it "should prune the proper number of partitions" do
expect(Time).to receive(:now).and_return(Time.utc(2015, 05, 02))
expect(subject).to receive(:partition_tables).and_return(%w{
partman_animals_2014_9
partman_animals_2014_10
partman_animals_2014_11
partman_animals_2014_12
partman_animals_2015_1
partman_animals_2015_2
partman_animals_2015_3
partman_animals_2015_4
partman_animals_2015_5
partman_animals_2015_6
})
expect(subject.base_class.connection).to receive(:drop_table).with('partman_animals_2014_9')
expect(subject.base_class.connection).to receive(:drop_table).with('partman_animals_2014_10')
subject.prune_partitions(6)
end
end
end

View File

@ -1,5 +1,5 @@
class AddFooToPartmanAnimals < CanvasPartman::Migration
self.master_table = :partman_animals
self.base_class = Animal
def self.up
with_each_partition do |partition_table_name|

View File

@ -1,5 +1,5 @@
class AddBarToPartmanAnimals < CanvasPartman::Migration
self.master_table = :partman_animals
self.base_class = Animal
def change
with_each_partition do |table_name|

View File

@ -1,5 +1,5 @@
class RemoveFooFromPartmanAnimals < CanvasPartman::Migration
self.master_table = :partman_animals
self.base_class = Animal
def self.up
with_each_partition do |partition_table_name|

View File

@ -3,7 +3,6 @@ module CanvasPartmanTest
end
class AddAnotherThingToPartmanAnimals < CanvasPartman::Migration
self.master_table = 'partman_animals'
self.base_class = CanvasPartmanTest::AnimalAlias
def self.up

View File

@ -1,5 +1,5 @@
class AddRaceIndexToPartmanAnimals < CanvasPartman::Migration
self.master_table = 'partman_animals'
self.base_class = Animal
def self.up
with_each_partition do |partition_table_name|