require 'activerecord/overflow_signalizer/version' require 'active_record' module ActiveRecord class OverflowSignalizer class Overflow < StandardError; end class UnsupportedType < StandardError; end DAY = 24 * 60 * 60 DEFAULT_AVG = 100_000 MAX_VALUE = { 'integer' => 2_147_483_647, 'bigint' => 9_223_372_036_854_775_807 }.freeze def initialize(logger: nil, models: nil, days_count: 60, signalizer: nil) @logger = logger || ActiveRecord::Base.logger @models = models || ActiveRecord::Base.descendants @days_count = days_count @signalizer = signalizer end def analyse! overflowed_tables = [] @models.group_by(&:table_name).each do |table, models| model = models.first next if model.abstract_class? || model.last.nil? pk = model.columns.select { |c| c.name == model.primary_key }.first max = MAX_VALUE.fetch(pk.sql_type) do |type| @logger.warn "Model #{model} has primary_key #{model.primary_key} with unsupported type #{type}" end if overflow_soon?(max, model) overflowed_tables << [table, model.last.public_send(pk.name), max] end end raise Overflow, overflow_message(overflowed_tables) if overflowed_tables.any? end def analyse analyse! rescue Overflow => e signalize(e.message) end private def overflow_soon?(max, model) if model.columns.select { |c| c.name == 'created_at' }.empty? (max - model.last.id) / DEFAULT_AVG <= @days_count else (max - model.last.id) / avg(model) <= @days_count end end def avg(model) to = model.last.created_at from = to - 7 * DAY amount = model.where(created_at: from..to).count average = amount / 7 average.zero? ? 1 : average end def overflow_message(overflowed_tables) overflowed = [] overflow_soon = [] overflowed_tables.each do |table, current_value, max_value| if current_value == max_value overflowed << table else overflow_soon << table end end "Owerflowed tables: #{overflowed}. Overflow soon tables: #{overflow_soon}" end def signalize(msg) if @logger && @logger.respond_to?(:warn) @logger.warn(msg) end if @signalizer && @signalizer.respond_to?(:signalize) @signalizer.signalize(msg) end end end end