diff options
Diffstat (limited to 'app/models/awareness_session.rb')
-rw-r--r-- | app/models/awareness_session.rb | 245 |
1 files changed, 0 insertions, 245 deletions
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb deleted file mode 100644 index 0b652984630..00000000000 --- a/app/models/awareness_session.rb +++ /dev/null @@ -1,245 +0,0 @@ -# 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| - Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - 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 - end - - nil - end - - def leave(user) - user_key = user_sessions_key(user.id) - - with_redis do |redis| - Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.pipelined do |pipeline| - pipeline.srem?(user_key, id_i) - pipeline.zrem(users_key, user.id) - end - 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 = - Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - redis.pipelined do |pipeline| - pipeline.scard(user_key) - pipeline.zcard(users_key) - end - end - - Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - 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 - 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 to_param - id&.to_s - end - - def to_s - "awareness_session=#{id}" - end - - def online_users_with_last_activity(threshold: PRESENCE_LIFETIME) - users_with_last_activity.filter do |_user, last_activity| - user_online?(last_activity, threshold: threshold) - end - end - - def users - User.where(id: user_ids) - end - - def users_with_last_activity - # where in (x, y, [...z]) is a set and does not maintain any order, we need - # to make sure to establish a stable order for both, the pairs returned from - # redis and the ActiveRecord query. Using IDs in ascending order. - user_ids, last_activities = user_ids_with_last_activity - .sort_by(&:first) - .transpose - - return [] if user_ids.blank? - - users = User.where(id: user_ids).order(id: :asc) - users.zip(last_activities) - end - - private - - attr_reader :id - - def user_online?(last_activity, threshold:) - last_activity.to_i + threshold.to_i > Time.zone.now.to_i - end - - # 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 |