92 行
3.2 KiB
Ruby
92 行
3.2 KiB
Ruby
require 'digest'
|
|
|
|
|
|
class SharedResourceIdAllocator
|
|
SPACES = {
|
|
'tags' => ['tags', 'external_tags'],
|
|
'wiki' => ['tag_descriptions', 'tag_groups'],
|
|
'wiki_history' => ['tag_description_versions', 'tag_group_versions'] }.freeze
|
|
HISTORY_IDS = {
|
|
'tags' => [
|
|
['tag_versions', 'tag_id'],
|
|
['nico_tag_versions', 'tag_id'],
|
|
['tag_description_versions', 'tag_id']],
|
|
'wiki' => [['tag_group_versions', 'tag_group_id']] }.freeze
|
|
LOCK_TIMEOUT = 10
|
|
|
|
def self.assign! record
|
|
space, tables = SPACES.find { |_name, names| names.include?(record.class.table_name) }
|
|
raise ArgumentError, 'Unknown shared ID resource' unless tables
|
|
|
|
connection = record.class.connection
|
|
transaction = record.class.current_transaction
|
|
unless transaction.open?
|
|
raise ActiveRecord::ActiveRecordError, 'Shared ID requires a transaction'
|
|
end
|
|
|
|
database = connection.pool.db_config.database
|
|
name = "btrc:ids:#{ Digest::SHA256.hexdigest(database)[0, 16] }:#{ space }"
|
|
quoted_name = connection.quote(name)
|
|
acquired = record.class.uncached do
|
|
connection.select_value("SELECT GET_LOCK(#{ quoted_name }, #{ LOCK_TIMEOUT })")
|
|
end
|
|
unless acquired.to_i == 1
|
|
raise ActiveRecord::LockWaitTimeout, "Shared ID lock unavailable: #{ space }"
|
|
end
|
|
|
|
release = lambda do
|
|
result = record.class.uncached do
|
|
connection.select_value("SELECT RELEASE_LOCK(#{ quoted_name })")
|
|
end
|
|
unless result.to_i == 1
|
|
raise ActiveRecord::ActiveRecordError, "Shared ID lock release failed: #{ space }"
|
|
end
|
|
end
|
|
begin
|
|
transaction.after_commit(&release)
|
|
transaction.after_rollback(&release)
|
|
rescue StandardError
|
|
release.call
|
|
raise
|
|
end
|
|
|
|
# 明示-tic な Id. 指定は許可する.
|
|
return if record.id.present?
|
|
|
|
next_id = record.class.uncached do
|
|
historical_max = HISTORY_IDS.fetch(space, []).map { |table, column|
|
|
quoted_table = connection.quote_table_name(table)
|
|
quoted_column = connection.quote_column_name(column)
|
|
connection.select_value("SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
|
|
}.max.to_i
|
|
|
|
[next_ids(connection, tables).max, historical_max + 1].max
|
|
end
|
|
|
|
record.id = next_id
|
|
end
|
|
|
|
def self.next_ids connection, tables
|
|
expiry = connection.select_value('SELECT @@SESSION.information_schema_stats_expiry').to_i
|
|
begin
|
|
connection.execute('SET SESSION information_schema_stats_expiry = 0')
|
|
quoted_tables = tables.map { connection.quote(_1) }.join(', ')
|
|
rows = connection.select_all(<<~SQL).to_a
|
|
SELECT TABLE_NAME, ENGINE, AUTO_INCREMENT
|
|
FROM information_schema.tables
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (#{ quoted_tables })
|
|
SQL
|
|
persistent = rows.all? do |row|
|
|
row['ENGINE'] == 'InnoDB' && row['AUTO_INCREMENT'].to_i.positive?
|
|
end
|
|
unless rows.size == tables.size && persistent
|
|
raise ActiveRecord::ActiveRecordError, 'Persistent InnoDB counters are required'
|
|
end
|
|
rows.map { _1.fetch('AUTO_INCREMENT').to_i }
|
|
ensure
|
|
connection.execute("SET SESSION information_schema_stats_expiry = #{ expiry }")
|
|
end
|
|
end
|
|
private_class_method :next_ids
|
|
end
|