summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/awareness_session.rb210
-rw-r--r--app/models/concerns/awareness.rb41
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb2
-rw-r--r--app/models/integrations/bamboo.rb1
-rw-r--r--app/models/integrations/base_issue_tracker.rb2
-rw-r--r--app/models/integrations/drone_ci.rb3
-rw-r--r--app/models/integrations/external_wiki.rb2
-rw-r--r--app/models/integrations/mock_ci.rb2
-rw-r--r--app/models/integrations/shimo.rb2
-rw-r--r--app/models/integrations/teamcity.rb5
-rw-r--r--app/models/integrations/unify_circuit.rb3
-rw-r--r--app/models/integrations/webex_teams.rb2
-rw-r--r--app/models/user.rb1
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