diff --git a/src/aging_session/event.clj b/src/aging_session/event.clj deleted file mode 100644 index 0e6492c..0000000 --- a/src/aging_session/event.clj +++ /dev/null @@ -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)))) diff --git a/src/aging_session/memory.clj b/src/aging_session/memory.clj index 97e5397..e683fd6 100644 --- a/src/aging_session/memory.clj +++ b/src/aging_session/memory.clj @@ -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)) diff --git a/test/aging_session/event_test.clj b/test/aging_session/event_test.clj deleted file mode 100644 index aa1eb9f..0000000 --- a/test/aging_session/event_test.clj +++ /dev/null @@ -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")))) diff --git a/test/aging_session/memory_test.clj b/test/aging_session/memory_test.clj index d5fac3d..9112dc7 100644 --- a/test/aging_session/memory_test.clj +++ b/test/aging_session/memory_test.clj @@ -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) diff --git a/test/aging_session/performance.clj b/test/aging_session/performance.clj index 582ae0d..5f4e3ee 100644 --- a/test/aging_session/performance.clj +++ b/test/aging_session/performance.clj @@ -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 []