From cb6c914754ffd8f668a935fada0a7e2aefb0f37f Mon Sep 17 00:00:00 2001 From: yosssi Date: Sun, 15 Jun 2014 01:15:10 +0900 Subject: [PATCH] Add a reaper --- Makefile | 2 + consts.go | 11 --- doc.go | 5 -- reaper/doc.go | 5 ++ reaper/options.go | 27 +++++++ reaper/reaper.go | 83 +++++++++++++++++++++ shared/consts.go | 15 ++++ shared/doc.go | 4 + {protobuf => shared/protobuf}/session.pb.go | 4 +- {protobuf => shared/protobuf}/session.proto | 0 shared/utils.go | 20 +++++ config.go => store/config.go | 14 ++-- store/doc.go | 5 ++ options.go => store/options.go | 3 +- session.go => store/session.go | 4 +- store.go => store/store.go | 58 +++++--------- 16 files changed, 190 insertions(+), 70 deletions(-) create mode 100644 Makefile delete mode 100644 consts.go delete mode 100644 doc.go create mode 100644 reaper/doc.go create mode 100644 reaper/options.go create mode 100644 reaper/reaper.go create mode 100644 shared/consts.go create mode 100644 shared/doc.go rename {protobuf => shared/protobuf}/session.pb.go (93%) rename {protobuf => shared/protobuf}/session.proto (100%) create mode 100644 shared/utils.go rename config.go => store/config.go (57%) create mode 100644 store/doc.go rename options.go => store/options.go (70%) rename session.go => store/session.go (82%) rename store.go => store/store.go (78%) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..da09e98 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +protoc: + protoc --gogo_out=. shared/protobuf/*.proto diff --git a/consts.go b/consts.go deleted file mode 100644 index 0f0b69c..0000000 --- a/consts.go +++ /dev/null @@ -1,11 +0,0 @@ -package boltstore - -import "time" - -const ( - defaultPath = "/" - defaultDBPath = "./sessions.db" - defaultBucketName = "sessions" - defaultBatchSize = 1000 - defaultCheckInterval = 10 * time.Second -) diff --git a/doc.go b/doc.go deleted file mode 100644 index 9a68b2d..0000000 --- a/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -/* -Package boltstore provides a session store backend -for gorilla/sessions using Bolt. -*/ -package boltstore diff --git a/reaper/doc.go b/reaper/doc.go new file mode 100644 index 0000000..df8177d --- /dev/null +++ b/reaper/doc.go @@ -0,0 +1,5 @@ +/* +Package reaper provides a reaper which removes +expired sessions. +*/ +package reaper diff --git a/reaper/options.go b/reaper/options.go new file mode 100644 index 0000000..eb8bfef --- /dev/null +++ b/reaper/options.go @@ -0,0 +1,27 @@ +package reaper + +import ( + "time" + + "github.com/yosssi/boltstore/shared" +) + +// Options represents options for the reaper. +type Options struct { + BucketName []byte + BatchSize int + CheckInterval time.Duration +} + +// setDefault sets default to the reaper options. +func (o *Options) setDefault() { + if o.BucketName == nil { + o.BucketName = []byte(shared.DefaultBucketName) + } + if o.BatchSize == 0 { + o.BatchSize = shared.DefaultBatchSize + } + if o.CheckInterval == 0 { + o.CheckInterval = shared.DefaultCheckInterval + } +} diff --git a/reaper/reaper.go b/reaper/reaper.go new file mode 100644 index 0000000..6df28d9 --- /dev/null +++ b/reaper/reaper.go @@ -0,0 +1,83 @@ +package reaper + +import ( + "log" + "time" + + "github.com/boltdb/bolt" + "github.com/yosssi/boltstore/shared" +) + +// Run invokes a reap function as a goroutine. +func Run(db *bolt.DB, options Options) (chan<- struct{}, <-chan struct{}) { + options.setDefault() + quitC, doneC := make(chan struct{}), make(chan struct{}) + go reap(db, options, quitC, doneC) + return quitC, doneC +} + +// Quit terminats the reap goroutine. +func Quit(quitC chan<- struct{}, doneC <-chan struct{}) { + quitC <- struct{}{} + <-doneC +} + +func reap(db *bolt.DB, options Options, quitC <-chan struct{}, doneC chan<- struct{}) { + var prevKey []byte + for { + err := db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket(options.BucketName) + if bucket == nil { + return nil + } + + c := bucket.Cursor() + + var i int + + for k, v := c.Seek(prevKey); ; k, v = c.Next() { + // If we hit the end of our sessions then + // exit and start over next time. + if k == nil { + prevKey = nil + return nil + } + + i++ + + session, err := shared.Session(v) + if err != nil { + return err + } + + if shared.Expired(session) { + err := db.Update(func(txu *bolt.Tx) error { + return txu.Bucket(options.BucketName).Delete(k) + }) + if err != nil { + return err + } + } + + if options.BatchSize == i { + copy(prevKey, k) + return nil + } + } + }) + + if err != nil { + log.Println(err.Error()) + } + + // Check if a quit signal is sent. + select { + case <-quitC: + doneC <- struct{}{} + return + default: + } + + time.Sleep(options.CheckInterval) + } +} diff --git a/shared/consts.go b/shared/consts.go new file mode 100644 index 0000000..a824751 --- /dev/null +++ b/shared/consts.go @@ -0,0 +1,15 @@ +package shared + +import "time" + +// Defaults for store.Options +const ( + DefaultPath = "/" + DefaultBucketName = "sessions" +) + +// Defaults for reaper.Options +const ( + DefaultBatchSize = 10 + DefaultCheckInterval = time.Second +) diff --git a/shared/doc.go b/shared/doc.go new file mode 100644 index 0000000..05270e2 --- /dev/null +++ b/shared/doc.go @@ -0,0 +1,4 @@ +/* +Package shared provides shared types, functions and constants. +*/ +package shared diff --git a/protobuf/session.pb.go b/shared/protobuf/session.pb.go similarity index 93% rename from protobuf/session.pb.go rename to shared/protobuf/session.pb.go index dfdc55f..fc4cdcd 100644 --- a/protobuf/session.pb.go +++ b/shared/protobuf/session.pb.go @@ -1,12 +1,12 @@ // Code generated by protoc-gen-gogo. -// source: session.proto +// source: shared/protobuf/session.proto // DO NOT EDIT! /* Package protobuf is a generated protocol buffer package. It is generated from these files: - session.proto + shared/protobuf/session.proto It has these top-level messages: Session diff --git a/protobuf/session.proto b/shared/protobuf/session.proto similarity index 100% rename from protobuf/session.proto rename to shared/protobuf/session.proto diff --git a/shared/utils.go b/shared/utils.go new file mode 100644 index 0000000..0401229 --- /dev/null +++ b/shared/utils.go @@ -0,0 +1,20 @@ +package shared + +import ( + "time" + + "code.google.com/p/gogoprotobuf/proto" + "github.com/yosssi/boltstore/shared/protobuf" +) + +// Session converts the byte slice to the session struct value. +func Session(data []byte) (protobuf.Session, error) { + session := protobuf.Session{} + err := proto.Unmarshal(data, &session) + return session, err +} + +// Expired checks if the session is expired. +func Expired(session protobuf.Session) bool { + return *session.ExpiresAt > 0 && *session.ExpiresAt <= time.Now().Unix() +} diff --git a/config.go b/store/config.go similarity index 57% rename from config.go rename to store/config.go index 60ae82e..b754ebd 100644 --- a/config.go +++ b/store/config.go @@ -1,6 +1,9 @@ -package boltstore +package store -import "github.com/gorilla/sessions" +import ( + "github.com/gorilla/sessions" + "github.com/yosssi/boltstore/shared" +) // Config represents a config for a session store. type Config struct { @@ -11,12 +14,9 @@ type Config struct { // setDefault sets default to the config. func (c *Config) setDefault() { if c.SessionOptions.Path == "" { - c.SessionOptions.Path = defaultPath - } - if c.DBOptions.Path == "" { - c.DBOptions.Path = defaultDBPath + c.SessionOptions.Path = shared.DefaultPath } if c.DBOptions.BucketName == nil { - c.DBOptions.BucketName = []byte(defaultBucketName) + c.DBOptions.BucketName = []byte(shared.DefaultBucketName) } } diff --git a/store/doc.go b/store/doc.go new file mode 100644 index 0000000..c9d56d1 --- /dev/null +++ b/store/doc.go @@ -0,0 +1,5 @@ +/* +Package store provides a session store backend +for gorilla/sessions using Bolt. +*/ +package store diff --git a/options.go b/store/options.go similarity index 70% rename from options.go rename to store/options.go index b983839..576d64b 100644 --- a/options.go +++ b/store/options.go @@ -1,7 +1,6 @@ -package boltstore +package store // Options represents options for a database. type Options struct { - Path string BucketName []byte } diff --git a/session.go b/store/session.go similarity index 82% rename from session.go rename to store/session.go index 77c9aa7..c7c0cc8 100644 --- a/session.go +++ b/store/session.go @@ -1,9 +1,9 @@ -package boltstore +package store import ( "time" - "github.com/yosssi/boltstore/protobuf" + "github.com/yosssi/boltstore/shared/protobuf" ) // NewSession creates and returns a session data. diff --git a/store.go b/store/store.go similarity index 78% rename from store.go rename to store/store.go index c1d9d80..ed9d853 100644 --- a/store.go +++ b/store/store.go @@ -1,4 +1,4 @@ -package boltstore +package store import ( "bytes" @@ -6,13 +6,12 @@ import ( "encoding/gob" "net/http" "strings" - "time" "code.google.com/p/gogoprotobuf/proto" "github.com/boltdb/bolt" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" - "github.com/yosssi/boltstore/protobuf" + "github.com/yosssi/boltstore/shared" ) // store represents a session store. @@ -69,39 +68,12 @@ func (s *store) Save(r *http.Request, w http.ResponseWriter, session *sessions.S return nil } -// Close closes the database. -func (s *store) Close() error { - return s.db.Close() -} - -// open Opens a database and sets it to the session store. -func (s *store) open() error { - // Open a database. - db, err := bolt.Open(s.config.DBOptions.Path, 0666) - if err != nil { - return err - } - // Create a bucket if it does not exist. - err = db.Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucketIfNotExists(s.config.DBOptions.BucketName) - if err != nil { - return err - } - return nil - }) - if err != nil { - return err - } - s.db = db - return nil -} - // load loads a session data from the database. // True is returned if there is a session data in the database. func (s *store) load(session *sessions.Session) (bool, error) { // exists represents whether a session data exists or not. var exists bool - err := s.db.Update(func(tx *bolt.Tx) error { + err := s.db.View(func(tx *bolt.Tx) error { id := []byte(session.ID) bucket := tx.Bucket(s.config.DBOptions.BucketName) // Get the session data. @@ -109,17 +81,16 @@ func (s *store) load(session *sessions.Session) (bool, error) { if data == nil { return nil } - sessionData := &protobuf.Session{} - // Convert the byte slice to the Session struct value. - if err := proto.Unmarshal(data, sessionData); err != nil { + sessionData, err := shared.Session(data) + if err != nil { return err } // Check the expiration of the session data. - if *sessionData.ExpiresAt > 0 && *sessionData.ExpiresAt < time.Now().Unix() { - if err := bucket.Delete(id); err != nil { - return err - } - return nil + if shared.Expired(sessionData) { + err := s.db.Update(func(txu *bolt.Tx) error { + return txu.Bucket(s.config.DBOptions.BucketName).Delete(id) + }) + return err } exists = true dec := gob.NewDecoder(bytes.NewBuffer(sessionData.Values)) @@ -158,13 +129,18 @@ func (s *store) save(session *sessions.Session) error { } // New creates and returns a session store. -func New(config Config, keyPairs ...[]byte) (*store, error) { +func New(db *bolt.DB, config Config, keyPairs ...[]byte) (*store, error) { config.setDefault() store := &store{ codecs: securecookie.CodecsFromPairs(keyPairs...), config: config, + db: db, } - if err := store.open(); err != nil { + err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(config.DBOptions.BucketName) + return err + }) + if err != nil { return nil, err } return store, nil