このコミットが含まれているのは:
2026-10-15 20:02:34 +09:00
コミット c142f08e6c
10個のファイルの変更、111行の追加、47行の削除
+48 -10
ファイルの表示
@@ -4,7 +4,14 @@ require 'digest'
class SharedResourceIdAllocator
SPACES = {
'tags' => ['tags', 'external_tags'],
'wiki' => ['tag_descriptions', 'tag_groups'] }.freeze
'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
@@ -13,7 +20,9 @@ class SharedResourceIdAllocator
connection = record.class.connection
transaction = record.class.current_transaction
raise ActiveRecord::ActiveRecordError, 'Shared ID requires a transaction' unless transaction.open?
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 }"
@@ -41,16 +50,45 @@ class SharedResourceIdAllocator
raise
end
return if record.id.present?
# Locking reads see committed maxima even after earlier repeatable reads.
maxima = record.class.uncached do
tables.map do |table|
next_id = record.class.uncached do
historical_max = HISTORY_IDS.fetch(space, []).map do |table, column|
quoted_table = connection.quote_table_name(table)
quoted_column = connection.quote_column_name(column)
connection.select_value(
"SELECT id FROM #{ quoted_table } ORDER BY id DESC LIMIT 1 FOR UPDATE").to_i
end
"SELECT MAX(#{ quoted_column }) FROM #{ quoted_table }").to_i
end.max.to_i
[next_ids(connection, tables).max, historical_max + 1].max
end
record.id = maxima.max + 1
if record.id.present?
if record.id < next_id
raise ActiveRecord::RecordNotUnique, 'Shared ID is below the reserved position'
end
else
record.id = next_id
end
# The INSERT advances InnoDB's persistent counter, including explicit IDs.
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