このコミットが含まれているのは:
2026-10-18 18:28:05 +09:00
コミット 569b9234c6
25個のファイルの変更、483行の追加、144行の削除
+46 -10
ファイルの表示
@@ -35,8 +35,18 @@ class TagVersionsController < ApplicationController
private
def serialise_versions rows, language_code
rows = rows.to_a
user_ids = rows.map(&:created_by_user_id).compact.uniq
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
parent_ids = rows.flat_map do |row|
split_parent_tag_ids(row.parent_tag_ids) +
split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
end.uniq
parents_by_id = Tag.includes(
:tag_names, :materials, :deerjikists, :tag_descriptions,
parents: [:tag_names, :materials, :deerjikists, :tag_descriptions])
.where(id: parent_ids).index_by(&:id)
parent_json = parents_by_id.transform_values { TagRepr.base(_1, language_code:) }
rows.map do |row|
current_names = names_for_locale(row.names, language_code)
@@ -47,18 +57,10 @@ class TagVersionsController < ApplicationController
cur_parent_tag_ids = split_parent_tag_ids(row.parent_tag_ids)
prev_parent_tag_ids = split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
all_parent_tag_ids = (cur_parent_tag_ids | prev_parent_tag_ids)
tags_by_id =
Tag
.includes(:tag_names, :materials, :tag_descriptions, parents: :tag_descriptions)
.where(id: all_parent_tag_ids)
.index_by(&:id)
parent_tags =
build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id)
.map do |h|
{ tag: TagRepr.base(tags_by_id[h[:tag_id]], language_code:),
{ tag: parent_json.fetch(h[:tag_id]),
type: h[:type] }
end
@@ -66,6 +68,7 @@ class TagVersionsController < ApplicationController
version_no: row.version_no,
event_type: row.event_type,
names: row.names,
name_changes: name_changes(row.names, row.attributes['prev_names']),
name: { current: current_names['name'], prev: previous_names['name'] },
category: { current: row.category, prev: row.attributes['prev_category'] },
deprecated_at: { current: row.deprecated_at&.iso8601,
@@ -97,11 +100,44 @@ class TagVersionsController < ApplicationController
def split_values(values) = values.to_s.split(/\s+/).reject(&:blank?)
def names_for_locale names, language_code
values = snapshot_names(names)[language_code]
values.is_a?(Hash) ? values : {}
end
def snapshot_names names
values = names.is_a?(String) ? JSON.parse(names) : names
values&.fetch(language_code, {}) || {}
values.is_a?(Hash) ? values : {}
rescue JSON::ParserError
{}
end
def name_changes current_names, previous_names
current = snapshot_names(current_names)
previous = snapshot_names(previous_names)
(current.keys | previous.keys).sort.filter_map do |language|
current_name = language_snapshot(current[language])
previous_name = language_snapshot(previous[language])
next if current_name == previous_name
{ language_code: language, current: current_name, prev: previous_name }
end
end
def language_snapshot entry
return nil unless entry.is_a?(Hash)
aliases = Array(entry['aliases']).filter_map do |value|
value = { 'name' => value } if value.is_a?(String)
next unless value.is_a?(Hash) && value['name'].is_a?(String)
{ name: value['name'], script_code: value['script_code'] }
end
aliases = aliases.uniq.sort_by { [_1[:name], _1[:script_code].to_s] }
{ name: entry['name'],
script_code: entry['script_code'],
auto_generated: entry['auto_generated'],
aliases: }
end
def split_parent_tag_ids(values) = split_values(values).map(&:to_i)
end
+8 -17
ファイルの表示
@@ -37,27 +37,18 @@ class WikiPagesController < ApplicationController
def autolink_candidates
locale = resolve_locale!
language = params.fetch(:language, locale.language_code)
language = 'ja' if language == 'und'
return head :not_found unless language.is_a?(String)
target_locale =
if language == 'und'
locale
else
Locale.where(language_code: language, deprecated_at: nil,
code: Locale.codes.values).order(:code).first
end
return head :not_found unless target_locale
return head :not_found unless Language.exists?(code: language)
candidates = []
unless language == 'und'
names = TagDescription.joins(tag: :tag_names)
.where(language_code: language, tag_names: { language_code: language })
.pluck('tag_descriptions.tag_id', 'tag_descriptions.id', 'tag_names.name')
candidates = names.map do |tag_id, description_id, name|
WikiPageRepr.autolink_tag(target_locale, tag_id:, description_id:, name:)
end
names = TagDescription.joins(tag: :tag_names)
.where(language_code: locale.language_code, tag_names: { language_code: language })
.pluck('tag_descriptions.tag_id', 'tag_descriptions.id', 'tag_names.name')
candidates = names.map do |tag_id, description_id, name|
WikiPageRepr.autolink_tag(locale, tag_id:, description_id:, name:, language:)
end
candidates.concat(TagGroup.order(:id).pluck(:id, :name).map do |id, name|
WikiPageRepr.autolink_group(target_locale, id:, name:, language:)
WikiPageRepr.autolink_group(locale, id:, name:, language:)
end)
render json: candidates
end
+12 -7
ファイルの表示
@@ -1,8 +1,7 @@
class Locale < ApplicationRecord
attr_accessor :certified_tag_names_path
before_save :lock_tag_creation!, if: :will_generate_tag_names?
after_save :generate_tag_names!, if: :needs_tag_names?
around_save :save_with_tag_names!, if: :will_generate_tag_names?
belongs_to :language, foreign_key: :language_code, primary_key: :code
belongs_to :script, foreign_key: :script_code, primary_key: :code
@@ -17,11 +16,17 @@ class Locale < ApplicationRecord
deprecated_at.nil? && (will_save_change_to_code? || will_save_change_to_deprecated_at?)
end
def lock_tag_creation! = SharedResourceIdAllocator.lock_space!(Tag, 'tags')
def save_with_tag_names!
completed = self.class.transaction(requires_new: true) do
SharedResourceIdAllocator.lock_space!(Tag, 'tags')
saved = yield
raise ActiveRecord::RecordNotSaved, 'Locale save was halted' unless saved
def needs_tag_names?
deprecated_at.nil? && (saved_change_to_code? || saved_change_to_deprecated_at?)
TagNameCompletion.for_locale!(self)
true
end
unless completed
raise ActiveRecord::RecordNotSaved, 'Locale tag completion was rolled back'
end
end
def generate_tag_names! = TagNameCompletion.for_locale!(self)
end
+5
ファイルの表示
@@ -111,6 +111,11 @@ class Tag < ApplicationRecord
def has_deerjikists = deerjikists.loaded? ? deerjikists.any? : deerjikists.exists?
def snapshot_aliases language_code
if tag_names.loaded?
return tag_names.select { _1.language_code == language_code && !_1.primary? }
.sort_by(&:name).map(&:name)
end
tag_names.where(language_code:, primary_flg: false).order(:name).pluck(:name)
end
+2 -2
ファイルの表示
@@ -27,13 +27,13 @@ module WikiPageRepr
TagGroupRepr.tag_group(tag_group).merge('resource_type' => 'tag_group')
end
def autolink_tag locale, tag_id:, description_id:, name:
def autolink_tag locale, tag_id:, description_id:, name:, language:
{ resource_type: 'tag_description',
resource_id: description_id,
tag_id:,
tag_description_id: description_id,
name:,
language_code: locale.language_code,
language_code: language,
locale_code: locale.code_before_type_cast }
end
+41 -8
ファイルの表示
@@ -13,7 +13,19 @@ class SharedResourceIdAllocator
['tag_description_versions', 'tag_id']],
'wiki' => [['tag_group_versions', 'tag_group_id']] }.freeze
LOCK_TIMEOUT = 10
TRANSACTION_LOCKS = ObjectSpace::WeakMap.new
CONNECTION_LOCKS = ObjectSpace::WeakMap.new
class LockCleanup
def initialize &finish
@finish = finish
end
def trigger_transactional_callbacks? = false
def before_committed! = nil
def committed!(**) = @finish.call(self)
def rolledback!(**) = @finish.call(self)
end
private_constant :LockCleanup
def self.assign! record
space, tables = SPACES.find { |_name, names| names.include?(record.class.table_name) }
@@ -42,14 +54,14 @@ class SharedResourceIdAllocator
raise ArgumentError, 'Unknown shared ID resource' unless SPACES.key?(space)
connection = record_class.connection
transaction = record_class.current_transaction
transaction = connection.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 }"
held_locks = TRANSACTION_LOCKS[transaction] ||= {}
held_locks = CONNECTION_LOCKS[connection] ||= {}
return if held_locks.key?(name)
quoted_name = connection.quote(name)
@@ -61,20 +73,41 @@ class SharedResourceIdAllocator
end
held_locks[name] = true
release = lambda do
held_locks.delete(name)
finish = lambda do |cleanup|
next unless held_locks.key?(name)
# Savepoint callbacks may run before the outer transaction ends.
# Keep one session lock and transfer its cleanup to the remaining parent.
parent = connection.current_transaction
if parent.open?
connection.add_transaction_record(cleanup)
next
end
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
held_locks.delete(name)
rescue StandardError
# Never return a session with an untracked advisory lock to the pool.
begin
connection.disconnect!
ensure
held_locks.clear
end
raise
end
begin
transaction.after_commit(&release)
transaction.after_rollback(&release)
# Rails finalises remaining records in ensure if another callback raises.
connection.add_transaction_record(LockCleanup.new(&finish))
rescue StandardError
release.call
begin
connection.disconnect!
ensure
held_locks.clear
end
raise
end
end