Class: Hoodoo::Services::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/hoodoo/services/services/session.rb

Overview

A container for functionality related to a context session.

Defined Under Namespace

Classes: MockDalliClient

Constant Summary collapse

TTL =

Time To Live: Number of seconds for which a session remains valid after being saved. Only applicable from the save time onwards in stores that support TTL such as Memcached - see #save_to_memcached.

172800

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Session

Create a new instance.

options

Optional Hash of options, described below.

Options are:

session_id

UUID of this session. If unset, a new UUID is generated for you. You can read the UUID with the #session_id accessor method.

caller_id

UUID of the Caller instance associated with this session. This can be set either now or later, but the session cannot be saved without it.

caller_version

Version of the Caller instance; defaults to zero.

memcached_host

Host for Memcached connections; required if you want to use the #load_from_memcached! or #save_to_memcached methods.



136
137
138
139
140
141
142
143
# File 'lib/hoodoo/services/services/session.rb', line 136

def initialize( options = {} )
  @created_at = Time.now.utc

  self.session_id     = options[ :session_id     ] || Hoodoo::UUID.generate()
  self.memcached_host = options[ :memcached_host ]
  self.caller_id      = options[ :caller_id      ]
  self.caller_version = options[ :caller_version ] || 0
end

Instance Attribute Details

#caller_idObject

A Session must always refer to a Caller instance by UUID.



33
34
35
# File 'lib/hoodoo/services/services/session.rb', line 33

def caller_id
  @caller_id
end

#caller_versionObject

Callers can change; if so, related sessions must be invalidated. This must be achieved by keeping a version count on the Caller. A session is associated with a particular Caller version and if the version changes, associated sessions are flushed.

If you change a Caller version in a Session, you really should call #save_to_memcached as soon as possible afterwards so that the change gets recognised in Memcached.



44
45
46
# File 'lib/hoodoo/services/services/session.rb', line 44

def caller_version
  @caller_version
end

#created_atObject (readonly)

The creation date of this session instance as a Time instance in UTC.



90
91
92
# File 'lib/hoodoo/services/services/session.rb', line 90

def created_at
  @created_at
end

#expires_atObject (readonly)

The expiry date for this session - the session should be considered expired at or after this date. Some session stores may support automatic expiry of session data, but there may be a small window between the expiry date passing and the store expiring the data; so always check the expiry.

Only set when the session is saved (or loaded from a representation that includes an existing expiry date). See e.g.:

  • #save_to_memcached

The value is a Time instance in UTC. If nil, the session has not yet been saved.



106
107
108
# File 'lib/hoodoo/services/services/session.rb', line 106

def expires_at
  @expires_at
end

#identityObject

An OpenStruct instance with session-creator defined key/value pairs that define the identity of the session holder. This is usually related to a Caller resource instance - see also Hoodoo::Data::Resources::Caller - and will often contain a Caller resource instance’s UUID, amongst other data.

The object describes “who the session’s owner is”.



54
55
56
# File 'lib/hoodoo/services/services/session.rb', line 54

def identity
  @identity
end

#memcached_hostObject

Connection IP address/port String for Memcached.

If you are using Memcached for a session store, you can set the Memcached connection host either through this accessor, or via the object’s constructor.



114
115
116
# File 'lib/hoodoo/services/services/session.rb', line 114

def memcached_host
  @memcached_host
end

#permissionsObject

A Hoodoo::Services::Permissions instance.

The instance describes “what the session is allowed to do”.



67
68
69
# File 'lib/hoodoo/services/services/session.rb', line 67

def permissions
  @permissions
end

#scopingObject

An OpenStruct instance with session-creator defined values that describe the scoping, that is, visbility of data, for the session. Its contents relate to service resource interface descriptions (see the DSL for Hoodoo::Services::Interface) and may be partially or entirely supported by the ActiveRecord finder extensions in Hoodoo::ActiveRecord::Finder.

The object describes the “data that the session can ‘see’”.



78
79
80
# File 'lib/hoodoo/services/services/session.rb', line 78

def scoping
  @scoping
end

#session_idObject

A Session must have its own UUID.



29
30
31
# File 'lib/hoodoo/services/services/session.rb', line 29

def session_id
  @session_id
end

Instance Method Details

#augment_with_permissions_for(interaction) ⇒ Object

Speciality interface usually only called by the middleware, or components closely related to the middleware.

Takes this session and creates a copy for an inter-resource call which adds any additional parameters that the calling interface says it needs in order to complete the currently handled action.

Through calling this method, the middleware implements the access permission functionality described by Hoodoo::Services::Interface#additional_permissions_for.

interaction

Hoodoo::Services::Middleware::Interaction instance describing the current interaction. This is for the request that a resource implementation *is handling* at the point it wants to make an inter-resource call

  • it is not data related to the target of that

call.

Returns:

  • Hoodoo::Services::Session instance if everything works OK; this may be the same as, or different from, the input session depending on whether or not there were any permissions that needed adding.

  • false if the session can’t be saved due to a mismatched caller version - the session must have become invalid during handling.

If the augmented session cannot be saved due to a Memcached problem, an exception is raised and the generic handler will turn this into a 500 response for the caller. At this time, we really can’t do much better than that since failure to save the augmented session means the inter-resource call cannot proceed; it’s an internal fault.



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/hoodoo/services/services/session.rb', line 491

def augment_with_permissions_for( interaction )

  # Set up some convenience variables

  interface = interaction.target_interface
  action    = interaction.requested_action

  # If there are no additional permissions for this action, just return
  # the current session back again.

  action                 = action.to_s()
  additional_permissions = ( interface.additional_permissions() || {} )[ action ]

  return self if additional_permissions.nil?

  # Otherwise, duplicate the session and its permissions (or generate
  # defaults) and merge the additional permissions.

  local_session     = self.dup()
  local_permissions = self.permissions ? self.permissions.dup() : Hoodoo::Services::Permissions.new

  local_permissions.merge!( additional_permissions.to_h() )

  # Make sure the new session has its own ID and set the updated
  # permissions. Then try to save it and return the result.

  local_session.session_id  = Hoodoo::UUID.generate()
  local_session.permissions = local_permissions

  case local_session.save_to_memcached()
    when :ok
      return local_session

    when :outdated
      # Caller version mismatch; original session is now outdated and invalid
      return false

    else # Couldn't save it
      raise "Unable to create interim session for inter-resource call from #{ interface.resource } / #{ action }"
  end
end

#delete_from_memcachedObject

Delete this session from Memcached. The Session object is not modified.

Returns a symbol:

  • :ok: The Session was deleted from Memcached successfully.

  • :not_found: This session was not found in Memcached.

  • :fail: The session data could not be deleted (Memcached problem).



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/hoodoo/services/services/session.rb', line 329

def delete_from_memcached
  begin

    mclient = self.class.connect_to_memcached( self.memcached_host() )

    if mclient.delete( self.session_id )
      return :ok
    else
      return :not_found
    end

  rescue Exception => exception

    # Log error and return nil if the session can't be parsed
    #
    Hoodoo::Services::Middleware.logger.warn(
      'Hoodoo::Services::Session\#delete_from_memcached: Session delete - connection fault',
      exception.to_s
    )

    return :fail
  end

end

#expired?Boolean

Has this session expired? Only valid if an expiry date is set; see #expires_at.

Returns true if the session has expired, or false if it has either not expired, or has no expiry date set yet.

Returns:

  • (Boolean)


360
361
362
363
364
365
# File 'lib/hoodoo/services/services/session.rb', line 360

def expired?
  exp = self.expires_at()
  now = Time.now.utc

  return ! ( exp.nil? || now < exp )
end

#from_h!(hash) ⇒ Object

Load session parameters from a given Hash, of the form set by #to_h.

If appropriate Hash keys are present, will set any or all of #session_id, #identity, #scoping and #permissions.



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/hoodoo/services/services/session.rb', line 415

def from_h!( hash )
  hash = Hoodoo::Utilities.stringify( hash )

  %w(

    session_id
    caller_id
    caller_version

  ).each do | property |
    value = hash[ property ]
    self.send( "#{ property }=", value ) unless value.nil?
  end

  %w(

    created_at
    expires_at

  ).each do | property |
    if hash.has_key?( property )
      begin
        instance_variable_set( "@#{ property }", Time.parse( hash[ property ] ).utc() )
      rescue => e
        # Invalid time given; keep existing date
      end
    end
  end

  %w(

    identity
    scoping

  ).each do | property |
    value = hash[ property ]
    self.send( "#{ property }=", OpenStruct.new( value ) ) unless value.nil?
  end

  value = hash[ 'permissions' ]
  self.permissions = Hoodoo::Services::Permissions.new( value ) unless value.nil?
end

#load_from_memcached!(sid) ⇒ Object

Load session data into this instance, overwriting instance values if the session is found. Raises an exception if there is a problem connecting to Memcached. A Memcached connection host must have been set through the constructor or #memcached_host accessor first.

sid

The Session UUID to look up.

Returns a symbol:

  • :ok: The session data was loaded OK and is valid.

  • :outdated: The session data was loaded, but is outdated; either the session has expired, or its Caller version mismatches the associated stored Caller version in Memcached.

  • :not_found: The session was not found.

  • :fail: The session data could not be loaded (Memcached problem).



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/hoodoo/services/services/session.rb', line 228

def load_from_memcached!( sid )
  begin
    mclient      = self.class.connect_to_memcached( self.memcached_host() )
    session_hash = mclient.get( sid )

    if session_hash.nil?
      return :not_found
    else
      self.from_h!( session_hash )
      return :outdated if self.expired?

      cv = load_caller_version_from_memcached( mclient, self.caller_id )

      if cv == nil || cv > self.caller_version
        return :outdated
      else
        return :ok
      end
    end

  rescue Exception => exception

    # Log error and return nil if the session can't be parsed
    #
    Hoodoo::Services::Middleware.logger.warn(
      'Hoodoo::Services::Session\#load_from_memcached!: Session loading failed - connection fault or session corrupt',
      exception.to_s
    )

  end

  return :fail
end

#save_to_memcachedObject

Save this session to Memcached, in a manner that will allow it to be loaded by #load_from_memcached! later.

A session can only be saved if it has a Caller ID - see #caller_id= or the options hash passed to the constructor.

The Hoodoo::Services::Session::TTL constant determines how long the key lives in Memcached.

Returns a symbol:

  • :ok: The session data was saved OK and is valid. There was either a Caller record with an earlier or matching value in Memcached, or no preexisting record of the Caller.

  • :outdated: The session data could not be saved because an existing Caller record was found in Memcached with a newer version than ‘this’ session, implying that the session is already outdated.

  • :fail: The session data could not be saved (Memcached problem).



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/hoodoo/services/services/session.rb', line 166

def save_to_memcached
  if self.caller_id.nil?
    raise 'Hoodoo::Services::Session\#save_to_memcached: Cannot save this session as it has no assigned Caller UUID'
  end

  begin
    mclient = self.class.connect_to_memcached( self.memcached_host() )

    # Try to update the Caller version in Memcached using this
    # Session's data. If this fails, the Caller version is out of
    # date or we couldn't talk to Memcached. Either way, bail out.

    result = update_caller_version_in_memcached( self.caller_id,
                                                 self.caller_version,
                                                 mclient )

    return result unless result.equal?( :ok )

    # Must set this before saving, even though the delay between
    # setting this value and Memcached actually saving the value
    # with a TTL will mean that Memcached expires the key slightly
    # *after* the time we record.

    @expires_at = ( ::Time.now + TTL ).utc()

    return :ok if mclient.set( self.session_id,
                               self.to_h(),
                               TTL )

  rescue Exception => exception

    # Log error and return nil if the session can't be parsed
    #
    Hoodoo::Services::Middleware.logger.warn(
      'Hoodoo::Services::Session\#save_to_memcached: Session saving failed - connection fault or session corrupt',
      exception.to_s
    )

  end

  return :fail
end

#to_hObject

Represent this session’s data as a Hash, for uses such as storage in Memcached or loading into another session instance. See also #from_h!.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/hoodoo/services/services/session.rb', line 371

def to_h
  hash = {}

  %w(

    session_id
    caller_id
    caller_version

  ).each do | property |
    value = self.send( property )
    hash[ property ] = value unless value.nil?
  end

  %w(

    created_at
    expires_at

  ).each do | property |
    value = self.send( property )
    hash[ property ] = value.iso8601() unless value.nil?
  end

  %w(

    identity
    scoping
    permissions

  ).each do | property |
    value = self.send( property )
    hash[ property ] = Hoodoo::Utilities.stringify( value.to_h() ) unless value.nil?
  end

  return hash
end

#update_caller_version_in_memcached(cid, cv, mclient = nil) ⇒ Object

Update the version of a given Caller in Memcached. This is done automatically when Sessions are saved to Memcached, but if external code alters any Callers independently, it MUST call here to keep Memcached records up to date.

If no cached version is in Memcached for the Caller, the method assumes it is being called for the first time for that Caller and writes the version it has to hand, rather than considering it an error condition.

cid

Caller UUID of the Caller record to update.

cv

New version to store (an Integer).

mclient

Optional Dalli::Client instance to use for talking to Memcached. If omitted, a connection is established for you. This is mostly an optimisation parameter, used by code which already has established a connection and wants to avoid creating another unnecessarily.

Returns a Symbol:

  • :ok: The Caller record was updated successfully.

  • :outdated: The Caller was already present in Memcached with a _higher version_ than the one you wanted to save. Your own local Caller data must therefore already be out of date.

  • :fail: The Caller could not be updated (Memcached problem).



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/hoodoo/services/services/session.rb', line 292

def update_caller_version_in_memcached( cid, cv, mclient = nil )
  begin
    mclient ||= self.class.connect_to_memcached( self.memcached_host() )

    cached_version = load_caller_version_from_memcached( mclient, cid )

    if cached_version != nil && cached_version > cv
      return :outdated
    elsif save_caller_version_to_memcached( mclient, cid, cv ) == true
      return :ok
    end

  rescue Exception => exception

    # Log error and return nil if the session can't be parsed
    #
    Hoodoo::Services::Middleware.logger.warn(
      'Hoodoo::Services::Session\#update_caller_version_in_memcached: Client version update - connection fault or corrupt record',
      exception.to_s
    )

  end

  return :fail
end