From 1ad21d029c6ff80fe7fda8f66bca041ec825fcd7 Mon Sep 17 00:00:00 2001 From: Matt Silverlock Date: Sat, 11 Jul 2015 11:11:09 +0800 Subject: [PATCH] feature: expired sessions can be deleted via a background goroutine. - Call defer store.StopCleanup(store.Cleanup(time.Minute * 5)) after store creation. - Does not break the existing API (optional, but recommended) - Based on https://github.com/yosssi/boltstore/reaper - Deletes expired sessions (where expireson > now()) - Includes tests --- README.md | 2 ++ cleanup.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ cleanup_test.go | 51 +++++++++++++++++++++++++++++++++++++++ pgstore_test.go | 5 ++-- 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 cleanup.go create mode 100644 cleanup_test.go diff --git a/README.md b/README.md index b335d77..7e66e94 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ See http://www.gorillatoolkit.org/pkg/sessions for full documentation on underly // Fetch new store. store := NewPGStore("postgres://user:password@127.0.0.1:5432/database?sslmode=verify-full", []byte("secret-key")) defer store.Close() + // Run a background goroutine to clean up expired sessions from the database. + defer store.StopCleanup(store.Cleanup(time.Minute * 5)) // Get a session. session, err = store.Get(req, "session-key") diff --git a/cleanup.go b/cleanup.go new file mode 100644 index 0000000..e15dbc3 --- /dev/null +++ b/cleanup.go @@ -0,0 +1,64 @@ +package pgstore + +import ( + "log" + "time" +) + +var defaultInterval = time.Minute * 5 + +// Cleanup runs a background goroutine every interval that deletes expired +// sessions from the database. +// +// The design is based on https://github.com/yosssi/boltstore +func (db *PGStore) Cleanup(interval time.Duration) (chan<- struct{}, <-chan struct{}) { + if interval <= 0 { + interval = defaultInterval + } + + quit, done := make(chan struct{}), make(chan struct{}) + go db.cleanup(interval, quit, done) + return quit, done +} + +// StopCleanup stops the background cleanup from running. +func (db *PGStore) StopCleanup(quit chan<- struct{}, done <-chan struct{}) { + quit <- struct{}{} + <-done +} + +// cleanup deletes expired sessions at set intervals. +func (db *PGStore) cleanup(interval time.Duration, quit <-chan struct{}, done chan<- struct{}) { + ticker := time.NewTicker(interval) + + defer func() { + ticker.Stop() + }() + + for { + select { + case <-quit: + // Handle the quit signal + done <- struct{}{} + return + case <-ticker.C: + // Delete expired sessions on each tick + err := db.deleteExpired() + if err != nil { + log.Printf("pgstore: unable to delete expired sessions: %v", err) + } + } + + } + +} + +// deleteExpired deletes expired sessions from the database. +func (db *PGStore) deleteExpired() error { + _, err := db.DbMap.Exec("DELETE FROM http_sessions WHERE expireson < now()") + if err != nil { + return err + } + + return nil +} diff --git a/cleanup_test.go b/cleanup_test.go new file mode 100644 index 0000000..6ef53ae --- /dev/null +++ b/cleanup_test.go @@ -0,0 +1,51 @@ +package pgstore + +import ( + "net/http" + "os" + "testing" + "time" +) + +func TestCleanup(t *testing.T) { + ss := NewPGStore(os.Getenv("PGSTORE_TEST_CONN"), []byte(secret)) + if ss == nil { + t.Fatal("This test requires a real database") + } + + defer ss.Close() + // Start the cleanup goroutine. + defer ss.StopCleanup(ss.Cleanup(time.Millisecond * 500)) + + req, err := http.NewRequest("GET", "http://www.example.com", nil) + if err != nil { + t.Fatal("Failed to create request", err) + } + + session, err := ss.Get(req, "newsess") + if err != nil { + t.Fatal("Failed to create session", err) + } + + // Expire the session + session.Options.MaxAge = 1 + + m := make(http.Header) + if err = ss.Save(req, headerOnlyResponseWriter(m), session); err != nil { + t.Fatal("failed to save session:", err.Error()) + } + + // Give the ticker a moment to run + time.Sleep(time.Second * 1) + + // SELECT expired sessions. We should get a zero-length result slice back. + var results []int64 + _, err = ss.DbMap.Select(&results, "SELECT id FROM http_sessions WHERE expireson < now()") + if err != nil { + t.Fatalf("failed to select expired sessions from DB: %v", err) + } + + if len(results) > 0 { + t.Fatalf("ticker did not delete expired sessions: want 0 got %v", len(results)) + } +} diff --git a/pgstore_test.go b/pgstore_test.go index 75a1a9e..2adbb7a 100644 --- a/pgstore_test.go +++ b/pgstore_test.go @@ -1,11 +1,12 @@ package pgstore import ( - "github.com/gorilla/securecookie" - "github.com/gorilla/sessions" "net/http" "os" "testing" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" ) type headerOnlyResponseWriter http.Header