require 'digest' class SharedResourceIdAllocator SPACES = { 'tags' => ['tags', 'external_tags'], 'wiki' => ['tag_descriptions', 'tag_groups'] }.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 raise ActiveRecord::ActiveRecordError, 'Shared ID requires a transaction' unless transaction.open? 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 return if record.id.present? # Locking reads see committed maxima even after earlier repeatable reads. maxima = record.class.uncached do tables.map do |table| quoted_table = connection.quote_table_name(table) connection.select_value( "SELECT id FROM #{ quoted_table } ORDER BY id DESC LIMIT 1 FOR UPDATE").to_i end end record.id = maxima.max + 1 end end