188 lines
6.5 KiB
Ruby
188 lines
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Issues
|
|
module Rebalancing
|
|
class State
|
|
REDIS_KEY_PREFIX = "gitlab:issues-position-rebalances"
|
|
CONCURRENT_RUNNING_REBALANCES_KEY = "#{REDIS_KEY_PREFIX}:running_rebalances"
|
|
RECENTLY_FINISHED_REBALANCE_PREFIX = "#{REDIS_KEY_PREFIX}:recently_finished"
|
|
|
|
REDIS_EXPIRY_TIME = 10.days
|
|
MAX_NUMBER_OF_CONCURRENT_REBALANCES = 5
|
|
NAMESPACE = 1
|
|
PROJECT = 2
|
|
|
|
def initialize(root_namespace, projects)
|
|
@root_namespace = root_namespace
|
|
@projects = projects
|
|
@rebalanced_container_type = @root_namespace.is_a?(Group) ? NAMESPACE : PROJECT
|
|
@rebalanced_container_id = @rebalanced_container_type == NAMESPACE ? @root_namespace.id : projects.take.id # rubocop:disable CodeReuse/ActiveRecord
|
|
end
|
|
|
|
def track_new_running_rebalance
|
|
with_redis do |redis|
|
|
redis.multi do |multi|
|
|
# we trigger re-balance for namespaces(groups) or specific user project
|
|
value = "#{rebalanced_container_type}/#{rebalanced_container_id}"
|
|
multi.sadd?(CONCURRENT_RUNNING_REBALANCES_KEY, value)
|
|
multi.expire(CONCURRENT_RUNNING_REBALANCES_KEY, REDIS_EXPIRY_TIME)
|
|
end
|
|
end
|
|
end
|
|
|
|
def concurrent_running_rebalances_count
|
|
with_redis { |redis| redis.scard(CONCURRENT_RUNNING_REBALANCES_KEY).to_i }
|
|
end
|
|
|
|
def rebalance_in_progress?
|
|
is_running = case rebalanced_container_type
|
|
when NAMESPACE
|
|
namespace_ids = self.class.current_rebalancing_containers.filter_map { |string| string.split("#{NAMESPACE}/").second.to_i }
|
|
namespace_ids.include?(root_namespace.id)
|
|
when PROJECT
|
|
project_ids = self.class.current_rebalancing_containers.filter_map { |string| string.split("#{PROJECT}/").second.to_i }
|
|
project_ids.include?(projects.take.id) # rubocop:disable CodeReuse/ActiveRecord
|
|
else
|
|
false
|
|
end
|
|
|
|
refresh_keys_expiration if is_running
|
|
|
|
is_running
|
|
end
|
|
|
|
def can_start_rebalance?
|
|
rebalance_in_progress? || too_many_rebalances_running?
|
|
end
|
|
|
|
def cache_issue_ids(issue_ids)
|
|
with_redis do |redis|
|
|
values = issue_ids.map { |issue| [issue.relative_position, issue.id] }
|
|
|
|
redis.multi do |multi|
|
|
multi.zadd(issue_ids_key, values) unless values.blank?
|
|
multi.expire(issue_ids_key, REDIS_EXPIRY_TIME)
|
|
end
|
|
end
|
|
end
|
|
|
|
def get_cached_issue_ids(index, limit)
|
|
with_redis do |redis|
|
|
redis.zrange(issue_ids_key, index, index + limit - 1)
|
|
end
|
|
end
|
|
|
|
def cache_current_index(index)
|
|
with_redis { |redis| redis.set(current_index_key, index, ex: REDIS_EXPIRY_TIME) }
|
|
end
|
|
|
|
def get_current_index
|
|
with_redis { |redis| redis.get(current_index_key).to_i }
|
|
end
|
|
|
|
def cache_current_project_id(project_id)
|
|
with_redis { |redis| redis.set(current_project_key, project_id, ex: REDIS_EXPIRY_TIME) }
|
|
end
|
|
|
|
def get_current_project_id
|
|
with_redis { |redis| redis.get(current_project_key) }
|
|
end
|
|
|
|
def issue_count
|
|
@issue_count ||= with_redis { |redis| redis.zcard(issue_ids_key) }
|
|
end
|
|
|
|
def remove_current_project_id_cache
|
|
with_redis { |redis| redis.del(current_project_key) }
|
|
end
|
|
|
|
def refresh_keys_expiration
|
|
with_redis do |redis|
|
|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
|
redis.multi do |multi|
|
|
multi.expire(issue_ids_key, REDIS_EXPIRY_TIME)
|
|
multi.expire(current_index_key, REDIS_EXPIRY_TIME)
|
|
multi.expire(current_project_key, REDIS_EXPIRY_TIME)
|
|
multi.expire(CONCURRENT_RUNNING_REBALANCES_KEY, REDIS_EXPIRY_TIME)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def cleanup_cache
|
|
value = "#{rebalanced_container_type}/#{rebalanced_container_id}"
|
|
|
|
with_redis do |redis|
|
|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
|
|
redis.multi do |multi|
|
|
multi.del(issue_ids_key)
|
|
multi.del(current_index_key)
|
|
multi.del(current_project_key)
|
|
multi.srem?(CONCURRENT_RUNNING_REBALANCES_KEY, value)
|
|
multi.set(self.class.recently_finished_key(rebalanced_container_type, rebalanced_container_id), true, ex: 1.hour)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.rebalance_recently_finished?(project_id, namespace_id)
|
|
container_id = project_id || namespace_id
|
|
container_type = project_id.present? ? PROJECT : NAMESPACE
|
|
|
|
Gitlab::Redis::SharedState.with { |redis| redis.get(recently_finished_key(container_type, container_id)) }
|
|
end
|
|
|
|
def self.fetch_rebalancing_groups_and_projects
|
|
namespace_ids = []
|
|
project_ids = []
|
|
|
|
current_rebalancing_containers.each do |string|
|
|
container_type, container_id = string.split('/', 2).map(&:to_i)
|
|
|
|
case container_type
|
|
when NAMESPACE
|
|
namespace_ids << container_id
|
|
when PROJECT
|
|
project_ids << container_id
|
|
end
|
|
end
|
|
|
|
[namespace_ids, project_ids]
|
|
end
|
|
|
|
private
|
|
|
|
def self.current_rebalancing_containers
|
|
Gitlab::Redis::SharedState.with { |redis| redis.smembers(CONCURRENT_RUNNING_REBALANCES_KEY) }
|
|
end
|
|
|
|
attr_accessor :root_namespace, :projects, :rebalanced_container_type, :rebalanced_container_id
|
|
|
|
def too_many_rebalances_running?
|
|
concurrent_running_rebalances_count <= MAX_NUMBER_OF_CONCURRENT_REBALANCES
|
|
end
|
|
|
|
def issue_ids_key
|
|
"#{REDIS_KEY_PREFIX}:#{root_namespace.id}"
|
|
end
|
|
|
|
def current_index_key
|
|
"#{issue_ids_key}:current_index"
|
|
end
|
|
|
|
def current_project_key
|
|
"#{issue_ids_key}:current_project_id"
|
|
end
|
|
|
|
def self.recently_finished_key(container_type, container_id)
|
|
"#{RECENTLY_FINISHED_REBALANCE_PREFIX}:#{container_type}:#{container_id}"
|
|
end
|
|
|
|
def with_redis(&blk)
|
|
Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|