summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Andrew <mail@timothyandrew.net>2017-02-07 00:32:35 +0530
committerTimothy Andrew <mail@timothyandrew.net>2017-02-24 16:50:20 +0530
commit8f01644ff4d25285475cbf053b140292ac50f225 (patch)
treee2404c12acef20865dabc3197c19b1f17f605d1a
parent8e684809765fa866a125c176327825ebc565f5b3 (diff)
downloadgitlab-ce-8f01644ff4d25285475cbf053b140292ac50f225.tar.gz
Implement review comments from @rymai and @yorickpeterse
1. Refactoring and specs in the `Uniquify` class. 2. Don't use the `AdvisoryLocking` class. Similar functionality is provided (backed by Redis) in the `ExclusiveLease` class.
-rw-r--r--app/models/concerns/uniquify.rb18
-rw-r--r--app/models/user.rb14
-rw-r--r--lib/gitlab/database/advisory_locking.rb70
-rw-r--r--spec/models/concerns/uniquify_spec.rb40
4 files changed, 61 insertions, 81 deletions
diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb
index 1485ab6ae99..6734472bc6d 100644
--- a/app/models/concerns/uniquify.rb
+++ b/app/models/concerns/uniquify.rb
@@ -6,19 +6,23 @@ class Uniquify
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def string(base, exists_fn)
+ @base = base
@counter = nil
- if base.respond_to?(:call)
- increment_counter! while exists_fn[base.call(@counter)]
- base.call(@counter)
- else
- increment_counter! while exists_fn["#{base}#{@counter}"]
- "#{base}#{@counter}"
- end
+ increment_counter! while exists_fn[base_string]
+ base_string
end
private
+ def base_string
+ if @base.respond_to?(:call)
+ @base.call(@counter)
+ else
+ "#{@base}#{@counter}"
+ end
+ end
+
def increment_counter!
@counter = @counter ? @counter.next : 1
end
diff --git a/app/models/user.rb b/app/models/user.rb
index f71d7e90d33..7207cdc4581 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -351,9 +351,15 @@ class User < ActiveRecord::Base
ghost_user ||
begin
# Since we only want a single ghost user in an instance, we use an
- # advisory lock to ensure than this block is never run concurrently.
- advisory_lock = Gitlab::Database::AdvisoryLocking.new(:ghost_user)
- advisory_lock.lock
+ # exclusive lease to ensure than this block is never run concurrently.
+ lease_key = "ghost_user_creation"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(1)
+ end
# Recheck if a ghost user is already present (one might have been)
# added between the time we last checked (first line of this method)
@@ -375,7 +381,7 @@ class User < ActiveRecord::Base
email: email, name: "Ghost User", state: :blocked, ghost: true
)
ensure
- advisory_lock.unlock
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
end
end
diff --git a/lib/gitlab/database/advisory_locking.rb b/lib/gitlab/database/advisory_locking.rb
deleted file mode 100644
index 950898288be..00000000000
--- a/lib/gitlab/database/advisory_locking.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# An advisory lock is an application-level database lock which isn't tied
-# to a specific table or row.
-#
-# Postgres names its advisory locks with integers, while MySQL uses strings.
-# We support both here by using a `LOCK_TYPES` map of symbols to integers.
-# The symbol (stringified) is used for MySQL, and the corresponding integer
-# is used for Postgres.
-module Gitlab
- module Database
- class AdvisoryLocking
- LOCK_TYPES = {
- ghost_user: 1
- }
-
- def initialize(lock_type)
- @lock_type = lock_type
- end
-
- def lock
- ensure_valid_lock_type!
-
- query =
- if Gitlab::Database.postgresql?
- Arel::SelectManager.new(ActiveRecord::Base).project(
- Arel::Nodes::NamedFunction.new("pg_advisory_lock", [LOCK_TYPES[@lock_type]])
- )
- elsif Gitlab::Database.mysql?
- Arel::SelectManager.new(ActiveRecord::Base).project(
- Arel::Nodes::NamedFunction.new("get_lock", [Arel.sql("'#{@lock_type}'"), -1])
- )
- end
-
- run_query(query)
- end
-
- def unlock
- ensure_valid_lock_type!
-
- query =
- if Gitlab::Database.postgresql?
- Arel::SelectManager.new(ActiveRecord::Base).project(
- Arel::Nodes::NamedFunction.new("pg_advisory_unlock", [LOCK_TYPES[@lock_type]])
- )
- elsif Gitlab::Database.mysql?
- Arel::SelectManager.new(ActiveRecord::Base).project(
- Arel::Nodes::NamedFunction.new("release_lock", [Arel.sql("'#{@lock_type}'")])
- )
- end
-
- run_query(query)
- end
-
- private
-
- def ensure_valid_lock_type!
- unless valid_lock_type?
- raise RuntimeError, "Trying to use an advisory lock with an invalid lock type, #{@lock_type}."
- end
- end
-
- def valid_lock_type?
- LOCK_TYPES.keys.include?(@lock_type)
- end
-
- def run_query(arel_query)
- ActiveRecord::Base.connection.execute(arel_query.to_sql)
- end
- end
- end
-end
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
new file mode 100644
index 00000000000..4e06a1f4c31
--- /dev/null
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Uniquify, models: true do
+ describe "#string" do
+ it 'returns the given string if it does not exist' do
+ uniquify = Uniquify.new
+
+ result = uniquify.string('test_string', -> (s) { false })
+
+ expect(result).to eq('test_string')
+ end
+
+ it 'returns the given string with a counter attached if the string exists' do
+ uniquify = Uniquify.new
+
+ result = uniquify.string('test_string', -> (s) { true if s == 'test_string' })
+
+ expect(result).to eq('test_string1')
+ end
+
+ it 'increments the counter for each candidate string that also exists' do
+ uniquify = Uniquify.new
+
+ result = uniquify.string('test_string', -> (s) { true if s == 'test_string' || s == 'test_string1' })
+
+ expect(result).to eq('test_string2')
+ end
+
+ it 'allows passing in a base function that defines the location of the counter' do
+ uniquify = Uniquify.new
+
+ result = uniquify.string(
+ -> (counter) { "test_#{counter}_string" },
+ -> (s) { true if s == 'test__string' }
+ )
+
+ expect(result).to eq('test_1_string')
+ end
+ end
+end