New support for timeout resolution optimization.

This commit is contained in:
Felipe Lessa 2015-05-28 17:04:05 -03:00
parent b12b3e7cd1
commit 43b431a6c3
7 changed files with 80 additions and 4 deletions

View File

@ -91,6 +91,22 @@ Any authentication mechanism is supported as long as it uses a
session variable. session variable.
## Storage optimizations
We provide the following storage optimizations:
* Empty sessions are not saved. This is done transparently:
just insert a session variable and the session will
materialize. Note that if your framework always creates a
CSRF token (e.g., Snap), then this optimization will not
apply you.
* You can set the timeout resolution. Requests made within the
timeout resolution that do not change any session variables
will not update the session on the database. By default the
timeout resolution is set to 10 minutes.
## Current limitations ## Current limitations
These limitations may be addressed in the future. Right now, These limitations may be addressed in the future. Right now,

View File

@ -11,6 +11,7 @@ module Web.ServerSession.Frontend.Snap
, setAuthKey , setAuthKey
, setIdleTimeout , setIdleTimeout
, setAbsoluteTimeout , setAbsoluteTimeout
, setTimeoutResolution
, setPersistentCookies , setPersistentCookies
, setHttpOnlyCookies , setHttpOnlyCookies
, setSecureCookies , setSecureCookies

View File

@ -22,6 +22,7 @@ module Web.ServerSession.Frontend.Wai
, setAuthKey , setAuthKey
, setIdleTimeout , setIdleTimeout
, setAbsoluteTimeout , setAbsoluteTimeout
, setTimeoutResolution
, setPersistentCookies , setPersistentCookies
, setHttpOnlyCookies , setHttpOnlyCookies
, setSecureCookies , setSecureCookies

View File

@ -11,6 +11,7 @@ module Web.ServerSession.Frontend.Yesod
, setAuthKey , setAuthKey
, setIdleTimeout , setIdleTimeout
, setAbsoluteTimeout , setAbsoluteTimeout
, setTimeoutResolution
, setPersistentCookies , setPersistentCookies
, setHttpOnlyCookies , setHttpOnlyCookies
, setSecureCookies , setSecureCookies

View File

@ -24,6 +24,7 @@ module Web.ServerSession.Core
, setAuthKey , setAuthKey
, setIdleTimeout , setIdleTimeout
, setAbsoluteTimeout , setAbsoluteTimeout
, setTimeoutResolution
, setPersistentCookies , setPersistentCookies
, setHttpOnlyCookies , setHttpOnlyCookies
, setSecureCookies , setSecureCookies

View File

@ -17,6 +17,7 @@ module Web.ServerSession.Core.Internal
, setAuthKey , setAuthKey
, setIdleTimeout , setIdleTimeout
, setAbsoluteTimeout , setAbsoluteTimeout
, setTimeoutResolution
, setPersistentCookies , setPersistentCookies
, setHttpOnlyCookies , setHttpOnlyCookies
, setSecureCookies , setSecureCookies
@ -45,7 +46,7 @@ import Data.ByteString (ByteString)
import Data.Maybe (catMaybes, fromMaybe, isJust) import Data.Maybe (catMaybes, fromMaybe, isJust)
import Data.Text (Text) import Data.Text (Text)
import Data.Time (UTCTime, getCurrentTime) import Data.Time (UTCTime, getCurrentTime)
import Data.Time.Clock (NominalDiffTime, addUTCTime) import Data.Time.Clock (NominalDiffTime, addUTCTime, diffUTCTime)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import Web.PathPieces (PathPiece(..)) import Web.PathPieces (PathPiece(..))
@ -249,6 +250,8 @@ instance E.Exception StorageException where
-- --
-- * Idle and absolute timeouts ('setIdleTimeout' and 'setAbsoluteTimeout'). -- * Idle and absolute timeouts ('setIdleTimeout' and 'setAbsoluteTimeout').
-- --
-- * Timeout resolution ('setTimeoutResolution').
--
-- * Whether cookies should be persistent -- * Whether cookies should be persistent
-- ('setPersistentCookies'), HTTP-only ('setHTTPOnlyCookies') -- ('setPersistentCookies'), HTTP-only ('setHTTPOnlyCookies')
-- and/or secure ('setSecureCookies'). -- and/or secure ('setSecureCookies').
@ -262,6 +265,7 @@ data State s =
, authKey :: !Text , authKey :: !Text
, idleTimeout :: !(Maybe NominalDiffTime) , idleTimeout :: !(Maybe NominalDiffTime)
, absoluteTimeout :: !(Maybe NominalDiffTime) , absoluteTimeout :: !(Maybe NominalDiffTime)
, timeoutResolution :: !(Maybe NominalDiffTime)
, persistentCookies :: !Bool , persistentCookies :: !Bool
, httpOnlyCookies :: !Bool , httpOnlyCookies :: !Bool
, secureCookies :: !Bool , secureCookies :: !Bool
@ -280,6 +284,7 @@ createState sto = do
, authKey = "_ID" , authKey = "_ID"
, idleTimeout = Just $ 60*60*24*7 -- 7 days , idleTimeout = Just $ 60*60*24*7 -- 7 days
, absoluteTimeout = Just $ 60*60*24*60 -- 60 days , absoluteTimeout = Just $ 60*60*24*60 -- 60 days
, timeoutResolution = Just $ 60*10 -- 10 minutes
, persistentCookies = True , persistentCookies = True
, httpOnlyCookies = True , httpOnlyCookies = True
, secureCookies = False , secureCookies = False
@ -339,6 +344,35 @@ setAbsoluteTimeout (Just d) _ | d <= 0 = error "serversession/setAbsoluteTimeout
setAbsoluteTimeout val state = state { absoluteTimeout = val } setAbsoluteTimeout val state = state { absoluteTimeout = val }
-- | Set the timeout resolution.
--
-- We need to save both the creation and last access times on
-- sessions in order to implement idle and absolute timeouts.
-- This means that we have to save the updated session on the
-- storage backend even if the request didn't change any session
-- variable, if only to update the last access time.
--
-- This setting provides an optimization where the session is not
-- updated on the storage backend provided that:
--
-- * No session variables were changed.
--
-- * The difference between the /current/ time and the last
-- /saved/ access time is less than the timeout resolution.
--
-- For example, with a timeout resolution of 1 minute, every
-- request that does not change the session variables within 1
-- minute of the last update will not generate any updates on the
-- storage backend.
--
-- If the timeout resolution is @Nothing@, then this optimization
-- becomes disabled and the session will always be updated.
--
-- Defaults to 10 minutes.
setTimeoutResolution :: Maybe NominalDiffTime -> State s -> State s
setTimeoutResolution (Just d) _ | d <= 0 = error "serversession/setTimeoutResolution: Resolution should be positive."
setTimeoutResolution val state = state { timeoutResolution = val }
-- | Set whether by default cookies should be persistent (@True@) or -- | Set whether by default cookies should be persistent (@True@) or
-- non-persistent (@False@). Persistent cookies are saved across -- non-persistent (@False@). Persistent cookies are saved across
-- browser sessions. Non-persistent cookies are discarded when -- browser sessions. Non-persistent cookies are discarded when
@ -521,7 +555,9 @@ decomposeSession state sm1 =
-- | Save a session on the database. If an old session is -- | Save a session on the database. If an old session is
-- supplied, it is replaced, otherwise a new session is -- supplied, it is replaced, otherwise a new session is
-- generated. If the session is empty, it is not saved and -- generated. If the session is empty, it is not saved and
-- @Nothing@ is returned. -- @Nothing@ is returned. If the timeout resolution optimization
-- is applied (cf. 'setTimeoutResolution'), the old session is
-- returned and no update is made.
saveSessionOnDb saveSessionOnDb
:: Storage s :: Storage s
=> State s => State s
@ -533,6 +569,14 @@ saveSessionOnDb _ _ Nothing (DecomposedSession Nothing _ m)
-- Return Nothing without doing anything whenever the session -- Return Nothing without doing anything whenever the session
-- is empty (including auth ID) and there was no prior session. -- is empty (including auth ID) and there was no prior session.
| M.null m = return Nothing | M.null m = return Nothing
saveSessionOnDb State { timeoutResolution = Just res } now (Just old) (DecomposedSession authId _ sessionMap)
-- If the data is the same and the old access time is within
-- the timeout resolution, just return the old session without
-- doing anything else.
| sessionData old == sessionMap &&
sessionAuthId old == authId &&
abs (diffUTCTime now (sessionAccessedAt old)) < res =
return (Just old)
saveSessionOnDb state now maybeInput DecomposedSession {..} = do saveSessionOnDb state now maybeInput DecomposedSession {..} = do
-- Generate properties if needed or take them from previous -- Generate properties if needed or take them from previous
-- saved session. -- saved session.

View File

@ -74,6 +74,7 @@ main = hspec $ parallel $ do
authKey stnull `shouldBe` "_ID" authKey stnull `shouldBe` "_ID"
idleTimeout stnull `shouldBe` Just (60*60*24*7) idleTimeout stnull `shouldBe` Just (60*60*24*7)
absoluteTimeout stnull `shouldBe` Just (60*60*24*60) absoluteTimeout stnull `shouldBe` Just (60*60*24*60)
timeoutResolution stnull `shouldBe` Just (60*10)
persistentCookies stnull `shouldBe` True persistentCookies stnull `shouldBe` True
httpOnlyCookies stnull `shouldBe` True httpOnlyCookies stnull `shouldBe` True
secureCookies stnull `shouldBe` False secureCookies stnull `shouldBe` False
@ -266,8 +267,19 @@ main = hspec $ parallel $ do
saveSessionOnDb st fakenow (Just oldSession) emptyDecomp `shouldReturn` Just newSession saveSessionOnDb st fakenow (Just oldSession) emptyDecomp `shouldReturn` Just newSession
getMockOperations sto `shouldReturn` [ReplaceSession newSession] getMockOperations sto `shouldReturn` [ReplaceSession newSession]
it "does not save session if only difference was accessedAt, and it was less than threshold" $ it "respects the timeout resolution" $ do
pendingWith "wishlist" (session1, sto, st) <- prepareSaveSessionOnDb
let d = DecomposedSession (sessionAuthId session1) DoNotForceInvalidate (sessionData session1)
saveSessionOnDb st fakenow (Just session1) d `shouldReturn` Just session1
getMockOperations sto `shouldReturn` []
let t i = TI.addUTCTime (res + i) (sessionAccessedAt session1)
Just res = timeoutResolution st
saveSessionOnDb st (t (-1)) (Just session1) d `shouldReturn` Just session1
getMockOperations sto `shouldReturn` []
-- We don't care about t 0, timeoutResolution is Maybe anyway.
let session2 = session1 { sessionAccessedAt = t 1 }
saveSessionOnDb st (t 1) (Just session1) d `shouldReturn` Just session2
getMockOperations sto `shouldReturn` [ReplaceSession session2]
describe "decomposeSession" $ do describe "decomposeSession" $ do
prop "it is sane when not finding auth key or force invalidate key" $ prop "it is sane when not finding auth key or force invalidate key" $