I had recently knocked out an authorisation and session state manager in Scala for a solution I'm working on. The problem was that it looked more like Java then Scala, so I am revisiting it with an eye to making it as functional as possible.
The first piece of work has been to create a data structure for handling session keys and their expiry. I built this using an immutable queue and an immutable hash-map. I was unable to find a satisfactory implementation of an immutable heap for use as a priority queue, perhaps they are just not that practical for use in an immutable context. The consequence is that the state may hold expired sessions in memory for longer than necessary, however no longer then twice the expiry time period.
I have developed this in Scala 2.10
trait SessionState[K,T] {
/**
* Expiry time of a session in milliseconds
* @return
*/
def expiry:Int
/**
* Returns the corresponding tag for the sessionkey
* @param sessionKey
* @param datetime current datetime in milliseconds
* @return
*/
def getSessionTag(sessionKey:K)(implicit datetime:Long):Try[T]
/**
* Rejuvenates the session increasing its expiry time to the datetime + the expiry time
* @param sessionKey
* @param datetime current datetime in milliseconds
* @return Success if session has not already expired, otherwise SessionExpiredException
*/
def refreshSession(sessionKey:K)(implicit datetime:Long):Try[SessionState[K,T]]
/**
* Adds a session key and tag
* @param sessionKey
* @param tag
* @param datetime current datetime in milliseconds
* @return New SessionState if successfull, otherwise SessionAlreadyExistsException
*/
def addSession(sessionKey:K, tag:T)(implicit datetime:Long):Try[SessionState[K,T]]
/**
* Adds a session key and tag
* @param sessionKey
* @param tag
* @param datetime current datetime in milliseconds
* @return New SessionState if successfull, otherwise SessionAlreadyExistsException
*/
def +(sessionKey:K, tag:T)(implicit datetime:Long):Try[SessionState[K,T]] = addSession(sessionKey, tag, datetime)
/**
* Removes the session key if found
* @param sessionKey
* @param datetime
* @return New SessionState with the session key removed
*/
def expireSession(sessionKey:K)(implicit datetime:Long):SessionState[K,T]
/**
* Removes the session key if found
* @param sessionKey
* @param datetime
* @return New SessionState with the session key removed
*/
def -(sessionKey:K)(implicit datetime:Long):SessionState[K,T] = expireSession(sessionKey)
}
For the implicit parameter I just used
implicit def currentTime:Long = new Date().getTime
and for the implementation I have
object SessionState {
def apply[K,T](expiry:Int):SessionState[K, T] = SessionStateInstance[K, T](expiry, Queue.empty, Map.empty)
}
private case class SessionStateInstance[K, T](expiry:Int, sessionQueue:Queue[(K, Long)], keysExpireAndTag:Map[K,(T,Long)]) extends SessionState[K,T] {
def getSessionTag(sessionKey:K)(implicit datetime:Long):Try[T] =
keysExpireAndTag.get(sessionKey) collect {
case (tag, expiry) if (expiry > datetime) => Success(tag)
} getOrElse(Failure(SessionExpiredException))
def refreshSession(sessionKey:K)(implicit datetime:Long):Try[SessionState[K,T]] =
keysExpireAndTag.get(sessionKey) collect {
case (tag, expiry) if (expiry > datetime) => {
val cleared = clearedExpiredSessions(datetime)
Success(SessionStateInstance(this.expiry, cleared.sessionQueue, cleared.keysExpireAndTag + (sessionKey -> (tag, expiry))))
}
} getOrElse(Failure(SessionExpiredException))
def addSession(sessionKey:K, tag:T)(implicit datetime:Long):Try[SessionState[K,T]] =
keysExpireAndTag.get(sessionKey) collect {
case (tag, expiry) if (expiry > datetime) => Failure(SessionAlreadyExistsException)
} getOrElse {
val cleared = clearedExpiredSessions(datetime)
Success(SessionStateInstance(this.expiry, cleared.sessionQueue.enqueue((sessionKey, datetime + expiry)), cleared.keysExpireAndTag + (sessionKey -> (tag, datetime + expiry))))
}
def expireSession(sessionKey:K)(implicit datetime:Long):SessionState[K,T] = {
val cleared = clearedExpiredSessions(datetime)
if (cleared.keysExpireAndTag.contains(sessionKey)) SessionStateInstance(this.expiry, cleared.sessionQueue, cleared.keysExpireAndTag - sessionKey)
else cleared
}
private def clearedExpiredSessions(datetime:Long):SessionStateInstance[K,T] = clearedExpiredSessions(datetime, sessionQueue, keysExpireAndTag)
private def clearedExpiredSessions(datetime:Long, sessionQueue:Queue[(K, Long)], keysExpireAndTag:Map[K,(T,Long)]):SessionStateInstance[K,T] = {
sessionQueue.headOption collect {
case (key, expiry) if (expiry < datetime) => keysExpireAndTag.get(key) map {
case (tag, expiry) if (expiry < datetime) => clearedExpiredSessions(datetime, sessionQueue.dequeue._2, keysExpireAndTag - key)
case (tag, expiry) => clearedExpiredSessions(datetime, sessionQueue.dequeue._2.enqueue((key, expiry)), keysExpireAndTag)
} getOrElse(clearedExpiredSessions(datetime, sessionQueue.dequeue._2, keysExpireAndTag))
} getOrElse (SessionStateInstance(this.expiry, sessionQueue, keysExpireAndTag))
}
}
case object SessionAlreadyExistsException extends Exception("Session key already exists")
case object SessionExpiredException extends Exception("Session key has already expired")