Module: Common::Client::Concerns::MhvLockedSessionClient

Extended by:
ActiveSupport::Concern
Includes:
SentryLogging
Included in:
MHVJwtSessionClient, MHVSessionBasedClient
Defined in:
lib/common/client/concerns/mhv_locked_session_client.rb

Overview

Module mixin for overriding session logic when making session-based client connections that should lock during session creation, to prevent threads from making simultaneous authentication API calls.

All references to “session” in this module refer to the upstream MHV session.

Constant Summary collapse

LOCK_RETRY_DELAY =

Number of seconds to wait between attempts to acquire a session lock

0.3
RETRY_ATTEMPTS =

How many times to attempt await of acquiring a session lock by a preceding request

40

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from SentryLogging

#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger, #set_sentry_metadata

Instance Attribute Details

#sessionHash (readonly)

Returns a hash containing session information.

Returns:

  • (Hash)

    a hash containing session information



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 18

module MhvLockedSessionClient
  extend ActiveSupport::Concern
  include SentryLogging

  LOCK_RETRY_DELAY = 0.3 # Number of seconds to wait between attempts to acquire a session lock
  RETRY_ATTEMPTS = 40 # How many times to attempt await of acquiring a session lock by a preceding request

  attr_reader :session

  ##
  # @param session [Hash] a hash containing a key with which the session will be found or built
  #
  def initialize(session:)
    refresh_session(session)
  end

  ##
  # Ensure the upstream MHV session is not expired or incomplete.
  #
  # @return [MhvFhirSessionClient] instance of `self`
  #
  def authenticate
    raise 'ICN is required for session creation' unless user_key

    iteration = 0

    # Loop unless a complete, valid MHV session exists, or until max_iterations is reached
    while invalid?(session) && iteration < RETRY_ATTEMPTS
      break if lock_and_get_session # Break out of the loop once a new session is created.

      sleep(LOCK_RETRY_DELAY)

      # Refresh the MHV session reference in case another thread has updated it.
      refresh_session(session)
      iteration += 1
    end
    if invalid?(session) && iteration >= RETRY_ATTEMPTS
      Rails.logger.info("Failed to create #{@client_session} after #{iteration} attempts to acquire lock")
    end

    self
  end

  ##
  # Override client_session method to use extended ::ClientSession classes
  #
  class_methods do
    ##
    # @return [MedicalRecords::ClientSession] if a MR (Medical Records) client session
    # @return [Rx::ClientSession] if an Rx (Prescription) client session
    # @return [SM::ClientSession] if a SM (Secure Messaging) client session
    #
    def client_session(klass = nil)
      @client_session ||= klass
    end
  end

  protected

  def refresh_session(session)
    @session = self.class.client_session.find_or_build(session)
  end

  def invalid?(session)
    session.expired?
  end

  private

  ##
  # Attempt to acquire a redis lock, then create a new MHV session. Once the session is created,
  # release the lock.
  #
  # return [Boolean] true if a session was created, otherwise false
  #
  def lock_and_get_session
    redis_lock = obtain_redis_lock
    if redis_lock
      begin
        @session = get_session
        return true
      ensure
        release_redis_lock(redis_lock)
      end
    end
    false
  end

  def obtain_redis_lock
    lock_key = "mhv_session_lock:#{user_key}"
    redis_lock = Redis::Namespace.new(REDIS_CONFIG[session_config_key][:namespace], redis: $redis)
    success = redis_lock.set(lock_key, 1, nx: true, ex: REDIS_CONFIG[session_config_key][:each_ttl])

    return redis_lock if success

    nil
  end

  def release_redis_lock(redis_lock)
    lock_key = "mhv_session_lock:#{user_key}"
    redis_lock.del(lock_key)
  end
end

Instance Method Details

#authenticateMhvFhirSessionClient

Ensure the upstream MHV session is not expired or incomplete.

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 39

def authenticate
  raise 'ICN is required for session creation' unless user_key

  iteration = 0

  # Loop unless a complete, valid MHV session exists, or until max_iterations is reached
  while invalid?(session) && iteration < RETRY_ATTEMPTS
    break if lock_and_get_session # Break out of the loop once a new session is created.

    sleep(LOCK_RETRY_DELAY)

    # Refresh the MHV session reference in case another thread has updated it.
    refresh_session(session)
    iteration += 1
  end
  if invalid?(session) && iteration >= RETRY_ATTEMPTS
    Rails.logger.info("Failed to create #{@client_session} after #{iteration} attempts to acquire lock")
  end

  self
end

#initialize(session:) ⇒ Object

Parameters:

  • session (Hash)

    a hash containing a key with which the session will be found or built



30
31
32
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 30

def initialize(session:)
  refresh_session(session)
end

#invalid?(session) ⇒ Boolean (protected)

Returns:

  • (Boolean)


81
82
83
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 81

def invalid?(session)
  session.expired?
end

#lock_and_get_sessionObject (private)

Attempt to acquire a redis lock, then create a new MHV session. Once the session is created, release the lock.

return [Boolean] true if a session was created, otherwise false



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 93

def lock_and_get_session
  redis_lock = obtain_redis_lock
  if redis_lock
    begin
      @session = get_session
      return true
    ensure
      release_redis_lock(redis_lock)
    end
  end
  false
end

#obtain_redis_lockObject (private)



106
107
108
109
110
111
112
113
114
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 106

def obtain_redis_lock
  lock_key = "mhv_session_lock:#{user_key}"
  redis_lock = Redis::Namespace.new(REDIS_CONFIG[session_config_key][:namespace], redis: $redis)
  success = redis_lock.set(lock_key, 1, nx: true, ex: REDIS_CONFIG[session_config_key][:each_ttl])

  return redis_lock if success

  nil
end

#refresh_session(session) ⇒ Object (protected)



77
78
79
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 77

def refresh_session(session)
  @session = self.class.client_session.find_or_build(session)
end

#release_redis_lock(redis_lock) ⇒ Object (private)



116
117
118
119
# File 'lib/common/client/concerns/mhv_locked_session_client.rb', line 116

def release_redis_lock(redis_lock)
  lock_key = "mhv_session_lock:#{user_key}"
  redis_lock.del(lock_key)
end