diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/awareness_session.rb | 210 | ||||
-rw-r--r-- | app/models/concerns/awareness.rb | 41 | ||||
-rw-r--r-- | app/models/concerns/integrations/slack_mattermost_notifier.rb | 2 | ||||
-rw-r--r-- | app/models/integrations/bamboo.rb | 1 | ||||
-rw-r--r-- | app/models/integrations/base_issue_tracker.rb | 2 | ||||
-rw-r--r-- | app/models/integrations/drone_ci.rb | 3 | ||||
-rw-r--r-- | app/models/integrations/external_wiki.rb | 2 | ||||
-rw-r--r-- | app/models/integrations/mock_ci.rb | 2 | ||||
-rw-r--r-- | app/models/integrations/shimo.rb | 2 | ||||
-rw-r--r-- | app/models/integrations/teamcity.rb | 5 | ||||
-rw-r--r-- | app/models/integrations/unify_circuit.rb | 3 | ||||
-rw-r--r-- | app/models/integrations/webex_teams.rb | 2 | ||||
-rw-r--r-- | app/models/user.rb | 1 |
13 files changed, 262 insertions, 14 deletions
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb new file mode 100644 index 00000000000..faadb7e0ba9 --- /dev/null +++ b/app/models/awareness_session.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +# A Redis backed session store for real-time collaboration. A session is defined +# by its documents and the users that join this session. An online user can have +# two states within the session: "active" and "away". +# +# By design, session must eventually be cleaned up. If this doesn't happen +# explicitly, all keys used within the session model must have an expiry +# timestamp set. +class AwarenessSession # rubocop:disable Gitlab/NamespacedClass + # An awareness session expires automatically after 1 hour of no activity + SESSION_LIFETIME = 1.hour + private_constant :SESSION_LIFETIME + + # Expire user awareness keys after some time of inactivity + USER_LIFETIME = 1.hour + private_constant :USER_LIFETIME + + PRESENCE_LIFETIME = 10.minutes + private_constant :PRESENCE_LIFETIME + + KEY_NAMESPACE = "gitlab:awareness" + private_constant :KEY_NAMESPACE + + class << self + def for(value = nil) + # Creates a unique value for situations where we have no unique value to + # create a session with. This could be when creating a new issue, a new + # merge request, etc. + value = SecureRandom.uuid unless value.present? + + # We use SHA-256 based session identifiers (similar to abbreviated git + # hashes). There is always a chance for Hash collisions (birthday + # problem), we therefore have to pick a good tradeoff between the amount + # of data stored and the probability of a collision. + # + # The approximate probability for a collision can be calculated: + # + # p ~= n^2 / 2m + # ~= (2^18)^2 / (2 * 16^15) + # ~= 2^36 / 2^61 + # + # n is the number of awareness sessions and m the number of possibilities + # for each item. For a hex number, this is 16^c, where c is the number of + # characters. With 260k (~2^18) sessions, the probability for a collision + # is ~2^-25. + # + # The number of 15 is selected carefully. The integer representation fits + # nicely into a signed 64 bit integer and eventually allows Redis to + # optimize its memory usage. 16 chars would exceed the space for + # this datatype. + id = Digest::SHA256.hexdigest(value.to_s)[0, 15] + + AwarenessSession.new(id) + end + end + + def initialize(id) + @id = id + end + + def join(user) + user_key = user_sessions_key(user.id) + + with_redis do |redis| + redis.pipelined do |pipeline| + pipeline.sadd(user_key, id_i) + pipeline.expire(user_key, USER_LIFETIME.to_i) + + pipeline.zadd(users_key, timestamp.to_f, user.id) + + # We also mark for expiry when a session key is created (first user joins), + # because some users might never actively leave a session and the key could + # therefore become stale, w/o us noticing. + reset_session_expiry(pipeline) + end + end + + nil + end + + def leave(user) + user_key = user_sessions_key(user.id) + + with_redis do |redis| + redis.pipelined do |pipeline| + pipeline.srem(user_key, id_i) + pipeline.zrem(users_key, user.id) + end + + # cleanup orphan sessions and users + # + # this needs to be a second pipeline due to the delete operations being + # dependent on the result of the cardinality checks + user_sessions_count, session_users_count = redis.pipelined do |pipeline| + pipeline.scard(user_key) + pipeline.zcard(users_key) + end + + redis.pipelined do |pipeline| + pipeline.del(user_key) unless user_sessions_count > 0 + + unless session_users_count > 0 + pipeline.del(users_key) + @id = nil + end + end + end + + nil + end + + def present?(user, threshold: PRESENCE_LIFETIME) + with_redis do |redis| + user_timestamp = redis.zscore(users_key, user.id) + break false unless user_timestamp.present? + + timestamp - user_timestamp < threshold + end + end + + def away?(user, threshold: PRESENCE_LIFETIME) + !present?(user, threshold: threshold) + end + + # Updates the last_activity timestamp for a user in this session + def touch!(user) + with_redis do |redis| + redis.pipelined do |pipeline| + pipeline.zadd(users_key, timestamp.to_f, user.id) + + # extend the session lifetime due to user activity + reset_session_expiry(pipeline) + end + end + + nil + end + + def size + with_redis do |redis| + redis.zcard(users_key) + end + end + + def users + User.where(id: user_ids) + end + + def users_with_last_activity + user_ids, last_activities = user_ids_with_last_activity.transpose + users = User.where(id: user_ids) + users.zip(last_activities) + end + + private + + attr_reader :id + + # converts session id from hex to integer representation + def id_i + Integer(id, 16) if id.present? + end + + def users_key + "#{KEY_NAMESPACE}:session:#{id}:users" + end + + def user_sessions_key(user_id) + "#{KEY_NAMESPACE}:user:#{user_id}:sessions" + end + + def with_redis + Gitlab::Redis::SharedState.with do |redis| + yield redis if block_given? + end + end + + def timestamp + Time.now.to_i + end + + def user_ids + with_redis do |redis| + redis.zrange(users_key, 0, -1) + end + end + + # Returns an array of tuples, where the first element in the tuple represents + # the user ID and the second part the last_activity timestamp. + def user_ids_with_last_activity + pairs = with_redis do |redis| + redis.zrange(users_key, 0, -1, with_scores: true) + end + + # map data type of score (float) to Time + pairs.map do |user_id, score| + [user_id, Time.zone.at(score.to_i)] + end + end + + # We want sessions to cleanup automatically after a certain period of + # inactivity. This sets the expiry timestamp for this session to + # [SESSION_LIFETIME]. + def reset_session_expiry(redis) + redis.expire(users_key, SESSION_LIFETIME) + + nil + end +end diff --git a/app/models/concerns/awareness.rb b/app/models/concerns/awareness.rb new file mode 100644 index 00000000000..da87d87e838 --- /dev/null +++ b/app/models/concerns/awareness.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Awareness + extend ActiveSupport::Concern + + KEY_NAMESPACE = "gitlab:awareness" + private_constant :KEY_NAMESPACE + + def join(session) + session.join(self) + + nil + end + + def leave(session) + session.leave(self) + + nil + end + + def session_ids + with_redis do |redis| + redis + .smembers(user_sessions_key) + # converts session ids from (internal) integer to hex presentation + .map { |key| key.to_i.to_s(16) } + end + end + + private + + def user_sessions_key + "#{KEY_NAMESPACE}:user:#{id}:sessions" + end + + def with_redis + Gitlab::Redis::SharedState.with do |redis| + yield redis if block_given? + end + end +end diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb index 3bdaa852ddf..142e62bb501 100644 --- a/app/models/concerns/integrations/slack_mattermost_notifier.rb +++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb @@ -34,7 +34,7 @@ module Integrations class HTTPClient def self.post(uri, params = {}) params.delete(:http_options) # these are internal to the client and we do not want them - Gitlab::HTTP.post(uri, body: params, use_read_total_timeout: true) + Gitlab::HTTP.post(uri, body: params) end end end diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index 4e30c1ccc69..230dc6bb336 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -155,7 +155,6 @@ module Integrations query_params[:os_authType] = 'basic' params[:basic_auth] = basic_auth - params[:use_read_total_timeout] = true params end diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb index bffe87c21ee..fe4a2f43b13 100644 --- a/app/models/integrations/base_issue_tracker.rb +++ b/app/models/integrations/base_issue_tracker.rb @@ -94,7 +94,7 @@ module Integrations result = false begin - response = Gitlab::HTTP.head(self.project_url, verify: true, use_read_total_timeout: true) + response = Gitlab::HTTP.head(self.project_url, verify: true) if response message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index 35524503dea..b1f72b7144e 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -60,8 +60,7 @@ module Integrations response = Gitlab::HTTP.try_get( commit_status_path(sha, ref), verify: enable_ssl_verification, - extra_log_info: { project_id: project_id }, - use_read_total_timeout: true + extra_log_info: { project_id: project_id } ) status = diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb index c16ae9926f1..bc2ea193a84 100644 --- a/app/models/integrations/external_wiki.rb +++ b/app/models/integrations/external_wiki.rb @@ -29,7 +29,7 @@ module Integrations end def execute(_data) - response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true) + response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true) response.body if response.code == 200 rescue StandardError nil diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb index 0b3a9bc5405..2d8e26d409f 100644 --- a/app/models/integrations/mock_ci.rb +++ b/app/models/integrations/mock_ci.rb @@ -49,7 +49,7 @@ module Integrations # # => 'running' # def commit_status(sha, ref) - response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification, use_read_total_timeout: true) + response = Gitlab::HTTP.get(commit_status_path(sha), verify: enable_ssl_verification) read_commit_status(response) rescue Errno::ECONNREFUSED :error diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb index dd25a0bc558..350ee61ad11 100644 --- a/app/models/integrations/shimo.rb +++ b/app/models/integrations/shimo.rb @@ -25,7 +25,7 @@ module Integrations # support for `test` method def execute(_data) - response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true) + response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true) response.body if response.code == 200 rescue StandardError nil diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index a23aa5f783d..e0299c9ac5f 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -156,7 +156,7 @@ module Integrations end def get_path(path) - Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true) + Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id }) end def post_to_build_queue(data, branch) @@ -167,8 +167,7 @@ module Integrations '</build>', headers: { 'Content-type' => 'application/xml' }, verify: enable_ssl_verification, - basic_auth: basic_auth, - use_read_total_timeout: true + basic_auth: basic_auth ) end diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index 646c2e75b03..f10a75fac5d 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -46,8 +46,7 @@ module Integrations response = Gitlab::HTTP.post(webhook, body: { subject: message.project_name, text: message.summary, - markdown: true, - use_read_total_timeout: true + markdown: true }.to_json) response if response.success? diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index 54d6f51ee17..75be457dcf5 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -44,7 +44,7 @@ module Integrations def notify(message, opts) header = { 'Content-Type' => 'application/json' } - response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json, use_read_total_timeout: true) + response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json) response if response.success? end diff --git a/app/models/user.rb b/app/models/user.rb index 2afd64358ef..cd4cc3537bb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ApplicationRecord include Gitlab::SQL::Pattern include AfterCommitQueue include Avatarable + include Awareness include Referable include Sortable include CaseSensitivity |