Skip to content

Instantly share code, notes, and snippets.

@simbo1905
Last active August 29, 2015 14:00
Show Gist options
  • Save simbo1905/11351545 to your computer and use it in GitHub Desktop.
Save simbo1905/11351545 to your computer and use it in GitHub Desktop.
minor fix to code from Session state in Scala,1. Immutable Session State at http://higher-state.blogspot.co.uk/2013/02/session-state-in-scala1-immutable.html
object SessionDemo {
def main(args: Array[String]) {
import scala.concurrent.duration._
val timeoutSeconds = 10
def usage() {
System.out.println(s"|timoutSeconds: ${timeoutSeconds}")
System.out.println(s"|commands:")
System.out.println(s"|+ <key> <value> // add a value under a key")
System.out.println(s"|- <key> // expire a value")
System.out.println(s"| <key> // get no refresh N.B. put ' ' before <key>")
System.out.println(s"|( <key> ) // get with refresh")
}
implicit def currentTime: Long = System.currentTimeMillis
var sessions: SessionState[String, String] = SessionState(10 seconds)
usage()
val debug = System.getProperty("debug", "false").toBoolean
val sc = new Scanner(System.in)
while (sc.hasNextLine()) {
if (debug) System.err.println(s"DEBUG before $sessions")
try {
val line = sc.nextLine()
val ab = line.split(" ")
line.charAt(0) match {
case '+' =>
if (ab.length == 3) {
val key = ab(1)
val value = ab(2)
sessions + (key, value) match {
case Success(newsessions) =>
sessions = newsessions
case Failure(ex) =>
System.err.println(s"Problem setting value: ${ex.getMessage}")
}
} else usage()
case '-' =>
if (ab.length == 2) {
val key = ab(1)
sessions = sessions - key
} else usage()
case '(' =>
if (ab.length == 3) {
val key = ab(1)
sessions = sessions(key) match {
case (newSessions, Some(value)) =>
println(value)
newSessions
case (newSessions, None) =>
println("None")
newSessions
}
} else usage()
case ' ' =>
val key = line.trim
sessions.getValueNoRefresh(key) match {
case Some(value) =>
println(value)
case None =>
println("None")
}
}
} catch {
case e: Exception => System.err.println(e)
}
if (debug) System.err.println(s"DEBUG after $sessions")
}
}
}
import org.scalatest._
import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success
import java.util.Scanner
class SessionSpec extends FlatSpec with Matchers {
"A SessionState" should "create a copy holding a value when you add to it" in {
implicit def currentTime: Long = System.currentTimeMillis
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 seconds)
val trySessionState = emptyState.put(key, value)
trySessionState match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
}
}
}
"A SessionState" should "create a copy holding a value when you add to it with plus opperator" in {
implicit def currentTime: Long = System.currentTimeMillis
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 seconds)
val trySessionState = emptyState + (key, value)
trySessionState match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
}
}
}
"A SessionState" should "be immutable" in {
implicit def currentTime: Long = System.currentTimeMillis
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 seconds)
val trySessionState = emptyState.put(key, value)
trySessionState match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
}
}
emptyState.getValueNoRefresh(key) match {
case None =>
// good
case Some(v) =>
fail()
}
}
"A SessionState" should "not allow duplicate entries" in {
implicit def currentTime: Long = System.currentTimeMillis
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 seconds)
val trySessionState = emptyState.put(key, value)
val newSessionState = trySessionState match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate
}
}
newSessionState.put(key, value) match {
case Failure(ex) => ex match {
case SessionAlreadyExistsException =>
// good
case _ =>
fail()
}
case Success(v) =>
fail()
}
}
"A SessionState" should "expire sessions on read" in {
var fakeTime = 0
implicit def currentTime: Long = {
fakeTime = fakeTime + 1
fakeTime
}
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(2 millisecond)
val firstSessionTry = emptyState.put(key, value)
val firstSession = firstSessionTry match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate
}
}
firstSession.getValueNoRefresh(key) match {
case Some(v) => fail()
case None => // good
}
}
"A SessionState" should "expire sessions on unrelated write" in {
var fakeTime = 0
implicit def currentTime: Long = {
fakeTime = fakeTime + 1
fakeTime
}
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 millisecond)
val firstSessionTry = emptyState.put(key, value)
val firstSession: SessionStateInstance[String, Long] = firstSessionTry match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate.asInstanceOf[SessionStateInstance[String, Long]]
}
}
firstSession.size should be(1)
fakeTime = 100
val secondSessionTry = firstSession.put("some-key", 987L)
val seondSession: SessionStateInstance[String, Long] = secondSessionTry match {
case Success(newsessions) =>
newsessions.asInstanceOf[SessionStateInstance[String, Long]]
case Failure(ex) => fail()
}
firstSession.size should be(1)
}
"A SessionState" should "expire upon expire" in {
implicit def currentTime: Long = System.currentTimeMillis
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 days)
val firstSession = emptyState.put(key, value) match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate
}
}
firstSession.expire(key) match {
case secondSession: SessionState[String, Long] => secondSession.getValueNoRefresh(key) match {
case Some(newsessions) =>
fail()
case None => // good
}
case _ => fail()
}
}
"A SessionState" should "expire sessions on expire with minus operator " in {
implicit def currentTime: Long = System.currentTimeMillis
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 hours)
val firstSessionTry = emptyState.put(key, value)
val firstSession: SessionStateInstance[String, Long] = firstSessionTry match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate.asInstanceOf[SessionStateInstance[String, Long]]
}
}
firstSession.size should be(1)
val secondSession = firstSession - key
secondSession.getValueNoRefresh(key) match {
case Some(newsessions) =>
fail()
case None => // good
}
}
"A SessionState" should "refresh expiry when getting with refresh" in {
var fakeTime = 0
implicit def currentTime: Long = {
fakeTime = fakeTime + 1
fakeTime
}
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 millisecond)
val firstSession = emptyState.put(key, value) match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate
}
}
fakeTime = 8
val secondSession = firstSession.getValueWithRefresh(key) match {
case (newSession, None) =>
fail
case (newSession, Some(v)) =>
v should be(value)
newSession
}
fakeTime = 17
secondSession.getValueNoRefresh(key) match {
case None =>
fail
case Some(v) =>
v should be(value)
}
}
"A SessionState" should "refresh expiry when getting with apply sugar" in {
var fakeTime = 0
implicit def currentTime: Long = {
fakeTime = fakeTime + 1
fakeTime
}
val key = "key0"
val value = 123L
val emptyState: SessionState[String, Long] = SessionState(10 millisecond)
val firstSession = emptyState.put(key, value) match {
case Failure(ex) =>
fail()
case Success(sessionstate) =>
sessionstate.getValueNoRefresh(key) match {
case None =>
fail()
case Some(v) =>
v should be(value)
sessionstate
}
}
fakeTime = 8
val tryOfRefresh = firstSession(key) // sugar
val secondSession = tryOfRefresh match {
case (newSession, None) =>
fail
case (session, Some(v)) =>
v should be(value)
session
}
fakeTime = 17
secondSession.getValueNoRefresh(key) match {
case None =>
fail
case Some(v) =>
v should be(value)
}
}
}
object SessionDemo {
def main(args: Array[String]) {
import scala.concurrent.duration._
val timeoutSeconds = 10
def usage() {
System.out.println(s"|timoutSeconds: ${timeoutSeconds}")
System.out.println(s"|commands:")
System.out.println(s"|+ <key> <value> // add a value under a key")
System.out.println(s"|- <key> // expire a value")
System.out.println(s"| <key> // get no refresh N.B. put ' ' before <key>")
System.out.println(s"|( <key> ) // get with refresh")
}
implicit def currentTime: Long = System.currentTimeMillis
var sessions: SessionState[String, String] = SessionState(10 seconds)
usage()
val debug = System.getProperty("debug", "false").toBoolean
val sc = new Scanner(System.in)
while (sc.hasNextLine()) {
if (debug) System.err.println(s"DEBUG before $sessions")
try {
val line = sc.nextLine()
val ab = line.split(" ")
line.charAt(0) match {
case '+' =>
if (ab.length == 3) {
val key = ab(1)
val value = ab(2)
sessions + (key, value) match {
case Success(newsessions) =>
sessions = newsessions
case Failure(ex) =>
System.err.println(s"Problem setting value: ${ex.getMessage}")
}
} else usage()
case '-' =>
if (ab.length == 2) {
val key = ab(1)
sessions = sessions - key
} else usage()
case '(' =>
if (ab.length == 3) {
val key = ab(1)
sessions = sessions(key) match {
case (newSessions, Some(value)) =>
println(value)
newSessions
case (newSessions, None) =>
println("None")
newSessions
}
} else usage()
case ' ' =>
val key = line.trim
sessions.getValueNoRefresh(key) match {
case Some(value) =>
println(value)
case None =>
println("None")
}
}
} catch {
case e: Exception => System.err.println(e)
}
if (debug) System.err.println(s"DEBUG after $sessions")
}
}
}
import scala.util.Try
import scala.collection.immutable.Vector
import scala.collection.immutable.Map
import scala.util.Failure
import scala.util.Success
import scala.concurrent.duration.FiniteDuration
/**
* SessionState[K, V] is an immutable key-value collection where
* entries expire if they are not accessed or refreshed within
* a given period.
* Typically the three operators "+", "-" and "apply" manipulate
* or access the data structure.
* Adapted with changes from Jamie Pullar's SessionState at
* http://higher-state.blogspot.co.uk/2013/02/session-state-in-scala1-immutable.html
*/
trait SessionState[K, V] {
/**
* Expiry time of entries in milliseconds
* @return
*/
def expiryInterval: Int
/**
* Returns the corresponding value for the key.
* Does not refresh the session expiry time. Consider using apply
* instead to increase the session time with mySessionState(someKey).
* @param key
* @param datetime current time in milliseconds
* @return
*/
def getValueNoRefresh(key: K)(implicit datetime: Long): Option[V]
/**
* Returns the corresponding value for the key and rejuvenates the
* session increasing its expiry time to (datetime+expiryInterval).
* Does rejuvenate the session expiry time. Overloaded as the "apply"
* method so that you can invoke as mySessionState(someKey).
* @param key
* @param datetime current datetime in milliseconds
* @return
*/
def getValueWithRefresh(key: K)(implicit datetime: Long): (SessionState[K, V], Option[V])
/**
* Returns the corresponding value for the key and rejuvenates the
* session increasing its expiry time to (datetime+ expiryInterval).
* Does rejuvenate the session expiry time. Overloaded as the "apply"
* method so that you can invoke as mySessionState(someKey).
* @param sessionKey
* @param datetime current datetime in milliseconds
* @return
*/
def apply(sessionKey: K)(implicit datetime: Long): (SessionState[K, V], Option[V]) =
getValueWithRefresh(sessionKey)
/**
* Adds a session value and under a session key.
* Does rejuvenate the session expiry time
* @param sessionKey
* @param value
* @param datetime current datetime in milliseconds
* @return New SessionState if successful, else SessionAlreadyExistsException
*/
def put(key: K, value: V)(implicit datetime: Long): Try[SessionState[K, V]]
/**
* @see Invokes put(key: K, value: V)
* @param sessionKey
* @param value
* @param datetime current datetime in milliseconds
* @return New SessionState if successfull, else SessionAlreadyExistsException
*/
def +(sessionKey: K, value: V)(implicit datetime: Long): Try[SessionState[K, V]] =
put(sessionKey, value)
/**
* Removes the session value if found
* @param key
* @param datetime
* @return New SessionState with the session key removed
*/
def expire(sessionKey: K)(implicit datetime: Long): SessionState[K, V]
/**
* Removes the session value if found
* @param key
* @param datetime
* @return New SessionState with the session key removed
*/
def -(sessionKey: K)(implicit datetime: Long): SessionState[K, V] =
expire(sessionKey)
}
object SessionState {
def apply[K, V](duration: FiniteDuration): SessionState[K, V] =
SessionStateInstance[K, V](duration.toMillis.toInt, Vector.empty, Map.empty)
}
private case class SessionStateInstance[K, V](expiryInterval: Int,
sessionVector: Vector[(K, Long)], valuesWithExpiryMap: Map[K, (V, Long)])
extends SessionState[K, V] {
// vanilla access no refresh of expiry
def getValueNoRefresh(sessionKey: K)(implicit datetime: Long): Option[V] =
valuesWithExpiryMap.get(sessionKey) collect {
case (value, expiry) if (expiry > datetime) => Some(value)
} getOrElse (None)
// gets the value and bumps the expiry by interval
def getValueWithRefresh(sessionKey: K)(implicit datetime: Long): (Sessions[K, V], Option[V]) = {
valuesWithExpiryMap.get(sessionKey) collect {
case (value, expiry) if (expiry > datetime) =>
(SessionStateInstance(this.expiryInterval, sessionVector,
valuesWithExpiryMap + (sessionKey ->
(value, datetime + this.expiryInterval))), Some(value))
} getOrElse {
(this, None)
}
}
// puts a value into the session and expires any old session keys
def put(sessionKey: K, value: V)(implicit datetime: Long): Try[SessionState[K, V]] =
valuesWithExpiryMap.get(sessionKey) collect {
case (value, expiry) if (expiry > datetime) =>
Failure(SessionAlreadyExistsException)
} getOrElse {
val cleared = clearedExpiredSessions(datetime)
Success(SessionStateInstance(this.expiryInterval,
cleared.sessionVector :+ (sessionKey, datetime + this.expiryInterval),
cleared.valuesWithExpiryMap + (sessionKey ->
(value, datetime + this.expiryInterval))))
}
// fast delete can leave the queue and map out of sync which will be fixed up on next clear operation
def expire(sessionKey: K)(implicit datetime: Long): SessionState[K, V] = {
val cleared = clearedExpiredSessions(datetime)
if (cleared.valuesWithExpiryMap.contains(sessionKey))
SessionStateInstance(this.expiryInterval,
cleared.sessionVector,
cleared.valuesWithExpiryMap - sessionKey)
else cleared
}
// used for unit testing
def size = {
valuesWithExpiryMap.size
}
private def clearedExpiredSessions(datetime: Long): SessionStateInstance[K, V] =
clearedExpiredSessions(datetime, sessionVector, valuesWithExpiryMap)
// forward scans the vector dropping expired values and puts it back
// into the same state as the updated map. halts when it finds unexpired
// values
private def clearedExpiredSessions(now: Long, sessionQueue: Vector[(K, Long)], valuesWithExpiryMap: Map[K, (V, Long)]): SessionStateInstance[K, V] = {
sessionQueue.headOption collect {
// if we have an expired key
case (key, end) if (end < now) =>
// double check with map
valuesWithExpiryMap.get(key) map {
case (_, expiry) if (expiry < now) =>
// drop the expired value
clearedExpiredSessions(now, sessionQueue.drop(1), valuesWithExpiryMap - key)
case (_, expiry) =>
// push the key to the back and possibly out of order which will delay collection by up to interval
clearedExpiredSessions(now, sessionQueue.drop(1) :+ (key, expiry), valuesWithExpiryMap)
} getOrElse {
// if it was not found in the map drop it from the front and check the next value
clearedExpiredSessions(now, sessionQueue.drop(1), valuesWithExpiryMap)
}
} getOrElse {
// no more expired keys at the front of the vector stop recursion
SessionStateInstance(this.expiryInterval, sessionQueue, valuesWithExpiryMap)
}
}
}
case object SessionAlreadyExistsException extends Exception("Session key already exists")
case object SessionNotFound extends Exception("Session key not found")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment