このコミットが含まれているのは:
@@ -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
|
||||
|
||||
@@ -40,15 +40,14 @@ class TagDescriptionOperation
|
||||
def self.record_version! description:, body:, user:
|
||||
creating = description.new_record?
|
||||
version_no = creating ? 1 : description.version_no + 1
|
||||
version = TagDescriptionVersion.create!(
|
||||
description.created_by_user_id = user.id if creating
|
||||
description.update!(body:, version_no:, updated_by_user_id: user.id)
|
||||
TagDescriptionVersion.create!(
|
||||
tag_id: description.tag_id, language_code: description.language_code,
|
||||
version_no:, event_type: creating ? 'create' : 'update', body:,
|
||||
source_language_code: description.source_language_code,
|
||||
source_version_no: description.source_version_no,
|
||||
created_by_user_id: user.id, created_at: Time.current)
|
||||
description.created_by_user_id = user.id if creating
|
||||
description.update!(body:, version_no:, updated_by_user_id: user.id)
|
||||
version
|
||||
end
|
||||
private_class_method :record_version!
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
class TagGroupOperation
|
||||
MAX_PLACEMENT_DEPTH = 5
|
||||
|
||||
def self.create!(name:, description:, members:, placements:, actor:)
|
||||
def self.create!(name:, description:, members:, placements:, actor:,
|
||||
require_compatibility: false)
|
||||
ApplicationRecord.transaction do
|
||||
group = TagGroup.new(version_no: 1, created_by_user: actor,
|
||||
updated_by_user: actor)
|
||||
@@ -10,27 +11,40 @@ class TagGroupOperation
|
||||
description: attributes.fetch(:description))
|
||||
group.save!
|
||||
persist_structure!(group:, attributes:, actor:)
|
||||
validate_compatibility!(group) if require_compatibility
|
||||
persist_version!(group:, attributes:, actor:, event_type: 'create')
|
||||
group
|
||||
end
|
||||
end
|
||||
|
||||
def self.update!(group:, name:, description:, members:, placements:, actor:)
|
||||
def self.update!(group:, name:, description:, members:, placements:, actor:,
|
||||
require_compatibility: false)
|
||||
ApplicationRecord.transaction do
|
||||
group.lock!
|
||||
attributes = prepare!(group:, name:, description:, members:, placements:)
|
||||
next group if unchanged?(group, attributes)
|
||||
if unchanged?(group, attributes)
|
||||
validate_compatibility!(group) if require_compatibility
|
||||
next group
|
||||
end
|
||||
|
||||
group.update!(name: attributes.fetch(:name),
|
||||
description: attributes.fetch(:description),
|
||||
version_no: group.version_no + 1,
|
||||
updated_by_user: actor)
|
||||
persist_structure!(group:, attributes:, actor:)
|
||||
validate_compatibility!(group) if require_compatibility
|
||||
persist_version!(group:, attributes:, actor:, event_type: 'update')
|
||||
group
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_compatibility! group
|
||||
TagGroupMarkdownRenderer.render(TagGroupRepr.one(group))
|
||||
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
|
||||
invalid!(group, :base, error.message)
|
||||
end
|
||||
private_class_method :validate_compatibility!
|
||||
|
||||
def self.prepare!(group:, name:, description:, members:, placements:)
|
||||
record = group
|
||||
normalized_name = name.to_s.strip
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする