Last active
August 29, 2015 14:00
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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