replace event functions with session expiry ttl, and tweak opts param

the events function stuff always seemed very overkill to me. i cannot
think of any other criteria i've ever wanted for session expiry other
than "expire based on some period of idle time elapsing" and an event
function being used to provide this seems a bit too much to me.

thusly, this has all been replaced with a simple ttl value (specified
in seconds) as a non-optional argument to aging-memory-store.

also change the aging-memory-store opts parameter, it must now be an
explicit map if provided
This commit is contained in:
Gered 2022-01-02 15:53:43 -05:00
parent 257c8caf10
commit a79fed3246
5 changed files with 99 additions and 106 deletions

View file

@ -1,7 +0,0 @@
(ns aging-session.event)
(defn expires-after
"Expires an entry if left untouched for a given number of seconds."
[seconds]
(let [ms (* 1000 seconds)]
(fn [now entry] (if-not (> (- now (:timestamp entry)) ms) entry))))

View file

@ -25,41 +25,42 @@
(assoc session-map key (new-entry data))))
(defn- update-entry
"Update a session entry based on event functions."
[event-fns ts [k v]]
(loop [entry v, fns (seq event-fns)]
(if (and entry fns)
(recur ((first fns) ts entry) (next fns))
(if entry [k entry]))))
"Update a session entry based on the configured session entry ttl."
[ttl now [k {:keys [timestamp] :as v}]]
(if v
(if-not (> (- now timestamp) ttl)
[k v])))
(defn- sweep-session
"Sweep the session and run all session functions."
[session-map event-fns]
(let [ts (now)]
(into {} (keep #(update-entry event-fns ts %) session-map))))
[session-map now ttl]
(into {} (keep #(update-entry ttl now %) session-map)))
(defn- sweep-entry
"Sweep a single entry."
[session-map event-fns key]
(if-let [[_ entry] (update-entry event-fns (now) [key (get session-map key)])]
(assoc session-map key entry)
(dissoc session-map key)))
[session-map now ttl key]
(if-let [existing-entry (get session-map key)]
(if-let [[_ entry] (update-entry ttl now [key existing-entry])]
(assoc session-map key entry)
(dissoc session-map key))
session-map))
(defprotocol AgingStore
(read-timestamp [store key]
"Read a session from the store and return its timestamp. If no key exists, returns nil."))
(defrecord MemoryAgingStore [session-map refresh-on-write refresh-on-read req-count req-limit event-fns]
(defrecord MemoryAgingStore [session-map ttl refresh-on-write refresh-on-read req-count req-limit]
AgingStore
(read-timestamp [_ key]
(get-in @session-map [key :timestamp]))
SessionStore
(read-session [_ key]
(swap! session-map sweep-entry event-fns key)
(when (and refresh-on-read (contains? @session-map key))
(swap! session-map assoc-in [key :timestamp] (now)))
(get-in @session-map [key :value]))
(let [ts (now)]
(swap! session-map sweep-entry ts ttl key)
(when (and refresh-on-read (contains? @session-map key))
(swap! session-map assoc-in [key :timestamp] ts))
(get-in @session-map [key :value])))
(write-session [_ key data]
(let [key (or key (str (UUID/randomUUID)))]
@ -75,12 +76,12 @@
(defn sweeper-thread
"Sweeper thread that watches the session and cleans it."
[{:keys [req-count req-limit session-map event-fns]} sweep-delay]
[{:keys [ttl req-count req-limit session-map]} sweep-delay]
(loop []
(when (>= @req-count req-limit)
(swap! session-map sweep-session event-fns)
(swap! session-map sweep-session (now) ttl)
(reset! req-count 0))
(. Thread (sleep sweep-delay)) ;; sleep for 30s
(Thread/sleep sweep-delay) ;; sleep for 30s
(recur)))
(defn in-thread
@ -89,17 +90,17 @@
(.start (Thread. ^Runnable f)))
(defn aging-memory-store
"Creates an in-memory session storage engine."
[& opts]
(let [{:keys [session-atom refresh-on-write refresh-on-read sweep-every sweep-delay events]
"Creates an in-memory session storage engine where entries expire after the given ttl"
[ttl & [opts]]
(let [{:keys [session-atom refresh-on-write refresh-on-read sweep-every sweep-delay]
:or {session-atom (atom {})
refresh-on-write false
refresh-on-read false
sweep-every 200
sweep-delay 30000
events []}} opts
sweep-delay 30000}} opts
ttl (* 1000 ttl) ; internally, we want ttl in milliseconds for convenience...
counter-atom (atom 0)
store (MemoryAgingStore. session-atom refresh-on-write refresh-on-read counter-atom sweep-every events)]
store (MemoryAgingStore. session-atom ttl refresh-on-write refresh-on-read counter-atom sweep-every)]
(in-thread #(sweeper-thread store sweep-delay))
store))

View file

@ -1,62 +0,0 @@
(ns aging-session.event_test
(:require
[clojure.test :refer :all]
[ring.middleware.session.store :refer :all]
[aging-session.event :as event]
[aging-session.memory :refer :all]))
(deftest session-expiry
(testing "Test session expiry."
; store where entries should expire after 1 second
(let [as (aging-memory-store :events [(event/expires-after 1)])]
(write-session as "mykey" {:foo 1})
(is (= (read-session as "mykey") {:foo 1})
"session entry was written")
(Thread/sleep 1500)
(is (nil? (read-session as "mykey"))
"session entry should no longer be present"))))
(deftest session-expiry-by-sweep
(testing "Test session expiry sweep."
(let [as (aging-memory-store
:events [(event/expires-after 1)] ; expire after 1 second
:sweep-every 5 ; only trigger sweep after 5 writes
:sweep-delay 1000 ; sweep thread tries to run every 1 second
)]
(write-session as "mykey" {:foo 1})
(Thread/sleep 1500)
; key should still exist, even though it's expired (not enough writes have occurred)
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
; key should exist for three more writes
(write-session as "other-key" {:foo 1})
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
(write-session as "other-key" {:foo 1})
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
(write-session as "other-key" {:foo 1})
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
; on the fifth write and after 1 second, key should not exist
(write-session as "other-key" {:foo 1})
(Thread/sleep 2000) ; allow time for sweeper thread to run
(is (nil? (read-timestamp as "mykey"))
"session entry should have been removed now"))))
(deftest refresh-on-read-nonexistant-key-then-sweep
(testing "Test an empty session read (with refresh-on-read enabled) then check that the expiry sweep still works"
(let [as (aging-memory-store
:events [(event/expires-after 1)] ; expire after 1 second
:refresh-on-read true
:sweep-every 1 ; sweep runs after every write
:sweep-delay 1000) ; sweep thread tries to run every 1 second
]
(is (nil? (read-session as "foo"))
"no session entry present for this key")
(Thread/sleep 1500)
; read again to trigger the sweep
(is (nil? (read-session as "foo"))
"still no session entry present for this key"))))

View file

@ -4,15 +4,19 @@
[ring.middleware.session.store :refer :all]
[aging-session.memory :refer :all]))
(defn ->basic-aging-memory-store
[& [opts]]
(aging-memory-store 30 opts))
(deftest basic-read-empty
(testing "Test session reads when there is no session value for that key."
(let [as (aging-memory-store)]
(let [as (->basic-aging-memory-store)]
(is (nil? (read-session as "mykey"))
"returns nil for non-existent session read"))))
(deftest basic-write
(testing "Test session writes and reads."
(let [as (aging-memory-store)]
(let [as (->basic-aging-memory-store)]
(write-session as "mykey" {:a 1})
(is (= (read-session as "mykey") {:a 1})
"session value was written")
@ -22,7 +26,7 @@
(deftest basic-delete
(testing "Test session delete."
(let [as (aging-memory-store)]
(let [as (->basic-aging-memory-store)]
(write-session as "mykey" {:a 1})
(is (= (read-session as "mykey") {:a 1})
"session value was written")
@ -32,7 +36,7 @@
(deftest timestamp-on-creation
(testing "Test the behaviour where each entry's timestamp is set only on session creation."
(let [as (aging-memory-store)]
(let [as (->basic-aging-memory-store)]
(write-session as "mykey" {:foo 1})
(let [ts1 (read-timestamp as "mykey")]
(is (integer? ts1)
@ -46,7 +50,7 @@
(deftest timestamp-on-write-only
(testing "Test the behaviour where each entry's timestamp is refreshed on write (not read)."
(let [as (aging-memory-store :refresh-on-write true)]
(let [as (->basic-aging-memory-store {:refresh-on-write true})]
(write-session as "mykey" {:foo 1})
(let [ts1 (read-timestamp as "mykey")]
(is (integer? ts1)
@ -65,7 +69,7 @@
(deftest timestamp-on-read-only
(testing "Test the behaviour where each entry's timestamp is refreshed on read (not write)."
(let [as (aging-memory-store :refresh-on-read true)]
(let [as (->basic-aging-memory-store {:refresh-on-read true})]
(write-session as "mykey" {:foo 1})
(let [ts1 (read-timestamp as "mykey")]
(is (integer? ts1)
@ -90,4 +94,62 @@
(is (not (= ts3 (read-timestamp as "mykey")))
"timestamp of the session entry was updated after its new value was read")))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(deftest session-expiry
(testing "Test session expiry."
; store where entries should expire after 1 second
(let [as (aging-memory-store 1)]
(write-session as "mykey" {:foo 1})
(is (= (read-session as "mykey") {:foo 1})
"session entry was written")
(Thread/sleep 1500)
(is (nil? (read-session as "mykey"))
"session entry should no longer be present"))))
(deftest session-expiry-by-sweep
(testing "Test session expiry sweep."
(let [as (aging-memory-store
1 ; expire after 1 second
{:sweep-every 5 ; only trigger sweep after 5 writes
:sweep-delay 1000 ; sweep thread tries to run every 1 second
})]
(write-session as "mykey" {:foo 1})
(Thread/sleep 1500)
; key should still exist, even though it's expired (not enough writes have occurred)
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
; key should exist for three more writes
(write-session as "other-key" {:foo 1})
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
(write-session as "other-key" {:foo 1})
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
(write-session as "other-key" {:foo 1})
(is (integer? (read-timestamp as "mykey"))
"session entry should still be present even though it has expired")
; on the fifth write and after 1 second, key should not exist
(write-session as "other-key" {:foo 1})
(Thread/sleep 2000) ; allow time for sweeper thread to run
(is (nil? (read-timestamp as "mykey"))
"session entry should have been removed now"))))
(deftest refresh-on-read-nonexistant-key-then-sweep
(testing "Test an empty session read (with refresh-on-read enabled) then check that the expiry sweep still works"
(let [as (aging-memory-store
1 ; expire after 1 second
{:refresh-on-read true
:sweep-every 1 ; sweep runs after every write
:sweep-delay 1000 ; sweep thread tries to run every 1 second
})]
(is (nil? (read-session as "foo"))
"no session entry present for this key")
(Thread/sleep 1500)
; read again to trigger the sweep
(is (nil? (read-session as "foo"))
"still no session entry present for this key"))))
#_(run-tests)

View file

@ -4,7 +4,6 @@
[criterium.core :refer [quick-bench]]
[ring.middleware.session.store :refer :all]
[ring.middleware.session.memory :refer [memory-store]]
[aging-session.event :as event]
[aging-session.memory :refer [aging-memory-store]]))
; these are copied from ring-ttl-session's benchmarks so that i can see how the performance of
@ -17,9 +16,9 @@
(defn ->aging-memory-store
[ttl]
(aging-memory-store
:refresh-on-write true
:refresh-on-read true
:events [(event/expires-after ttl)]))
ttl
{:refresh-on-write true
:refresh-on-read true}))
(defn check-nonexistent-read
[]