Update sessions/sessiondb/badger to its latest version and SessionController is able to initialize itself if session manager is not provided by the caller-dev.

Changes:

Update the badger database using transactions, as supported from yesterday via: 06242925c2

Add a custom `OnActivate` via mvc/activator/activate_listener.go, this can be used to perform any custom actions when the app registers the supported Controllers. See mvc/session_controller.go for the excellent use case.

errors.Reporter.AddErr returns true if the error is added to the stack, otherwise false


Former-commit-id: c896a2d186a4315643f3c5fdb4325f7ee48a9e0a
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-10-06 01:19:10 +03:00
parent 6925ed9b33
commit 4fb78bbcd9
17 changed files with 232 additions and 70 deletions

File diff suppressed because one or more lines are too long

View File

@ -23,7 +23,8 @@ func (u *VisitController) Get() {
u.Session.Set("visits", visits) u.Session.Set("visits", visits)
// write the current, updated visits // write the current, updated visits
u.Ctx.Writef("%d visits in %0.1f seconds", visits, time.Now().Sub(u.StartTime).Seconds()) u.Ctx.Writef("%d visit from my current session in %0.1f seconds of server's up-time",
visits, time.Now().Sub(u.StartTime).Seconds())
} }
func main() { func main() {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -56,23 +56,33 @@ func NewReporter() *Reporter {
// AddErr adds an error to the error stack. // AddErr adds an error to the error stack.
// if "err" is a StackError then // if "err" is a StackError then
// each of these errors will be printed as individual. // each of these errors will be printed as individual.
func (r *Reporter) AddErr(err error) { //
// Returns true if this "err" is not nil and it's added to the reporter's stack.
func (r *Reporter) AddErr(err error) bool {
if err == nil { if err == nil {
return return false
} }
if stackErr, ok := err.(StackError); ok { if stackErr, ok := err.(StackError); ok {
r.addStack(stackErr.Stack()) r.addStack(stackErr.Stack())
return } else {
}
r.mu.Lock() r.mu.Lock()
r.wrapper = r.wrapper.AppendErr(err) r.wrapper = r.wrapper.AppendErr(err)
r.mu.Unlock() r.mu.Unlock()
} }
return true
}
// Add adds a formatted message as an error to the error stack. // Add adds a formatted message as an error to the error stack.
func (r *Reporter) Add(format string, a ...interface{}) { //
// Returns true if this "err" is not nil and it's added to the reporter's stack.
func (r *Reporter) Add(format string, a ...interface{}) bool {
if format == "" && len(a) == 0 {
return false
}
// usually used as: "module: %v", err so // usually used as: "module: %v", err so
// check if the first argument is error and if that error is empty then don't add it. // check if the first argument is error and if that error is empty then don't add it.
if len(a) > 0 { if len(a) > 0 {
@ -81,7 +91,7 @@ func (r *Reporter) Add(format string, a ...interface{}) {
Error() string Error() string
}); ok { }); ok {
if e.Error() == "" { if e.Error() == "" {
return return false
} }
} }
} }
@ -89,6 +99,7 @@ func (r *Reporter) Add(format string, a ...interface{}) {
r.mu.Lock() r.mu.Lock()
r.wrapper = r.wrapper.Append(format, a...) r.wrapper = r.wrapper.Append(format, a...)
r.mu.Unlock() r.mu.Unlock()
return true
} }
// Describe same as `Add` but if "err" is nil then it does nothing. // Describe same as `Add` but if "err" is nil then it does nothing.

View File

@ -545,12 +545,6 @@ func (api *APIBuilder) Controller(relativePath string, controller activator.Base
api.reporter.Add("%v for path: '%s'", err, relativePath) api.reporter.Add("%v for path: '%s'", err, relativePath)
} }
if cInit, ok := controller.(interface {
Init(activator.RegisterFunc)
}); ok {
cInit.Init(registerFunc)
}
return return
} }

View File

@ -9,13 +9,6 @@ import (
"github.com/kataras/iris/mvc" "github.com/kataras/iris/mvc"
) )
// TODO: When go 1.9 will be released
// split this file in order to separate the concepts.
//
// Files should change after go1.9 final release:
// README.md: Hello World with Go 1.9
// core/host/supervisor.go
// context.go
type ( type (
// Context is the midle-man server's "object" for the clients. // Context is the midle-man server's "object" for the clients.
// //

View File

@ -0,0 +1,78 @@
package activator
import (
"reflect"
)
// CallOnActivate simply calls the "controller"'s `OnActivate(*ActivatePayload)` function,
// if any.
//
// Look `activator.go#Register` and `ActivateListener` for more.
func CallOnActivate(controller interface{},
bindValues *[]interface{}, registerFunc RegisterFunc) {
if ac, ok := controller.(ActivateListener); ok {
p := &ActivatePayload{
BindValues: bindValues,
Handle: registerFunc,
}
ac.OnActivate(p)
}
}
// ActivateListener is an interface which should be declared
// on a Controller which needs to register or change the bind values
// that the caller-"user" has been passed to; via the `app.Controller`.
// If that interface is completed by a controller
// then the `OnActivate` function will be called ONCE, NOT in every request
// but ONCE at the application's lifecycle.
type ActivateListener interface {
// OnActivate accepts a pointer to the `ActivatePayload`.
//
// The `Controller` can make use of the `OnActivate` function
// to register custom routes
// or modify the provided values that will be binded to the
// controller later on.
//
// Look `ActivatePayload` for more.
OnActivate(*ActivatePayload)
}
// ActivatePayload contains the necessary information and the ability
// to alt a controller's registration options, i.e the binder.
//
// With `ActivatePayload` the `Controller` can register custom routes
// or modify the provided values that will be binded to the
// controller later on.
type ActivatePayload struct {
BindValues *[]interface{}
Handle RegisterFunc
}
// EnsureBindValue will make sure that this "bindValue"
// will be registered to the controller's binder
// if its type is not already passed by the caller..
//
// For example, on `SessionController` it looks if *sessions.Sessions
// has been binded from the caller and if not then the "bindValue"
// will be binded and used as a default sessions manager instead.
//
// At general, if the caller has already provided a value with the same Type
// then the "bindValue" will be ignored and not be added to the controller's bind values.
//
// Returns true if the caller has NOT already provided a value with the same Type
// and "bindValue" is NOT ignored therefore is appended to the controller's bind values.
func (i *ActivatePayload) EnsureBindValue(bindValue interface{}) bool {
valueTyp := reflect.TypeOf(bindValue)
localBindValues := *i.BindValues
for _, bindedValue := range localBindValues {
// type already exists, remember: binding here is per-type.
if reflect.TypeOf(bindedValue) == valueTyp {
return false
}
}
*i.BindValues = append(localBindValues, bindValue)
return true
}

View File

@ -65,7 +65,6 @@ type BaseController interface {
// ActivateController returns a new controller type info description. // ActivateController returns a new controller type info description.
func ActivateController(base BaseController, bindValues []interface{}) (TController, error) { func ActivateController(base BaseController, bindValues []interface{}) (TController, error) {
// get and save the type. // get and save the type.
typ := reflect.TypeOf(base) typ := reflect.TypeOf(base)
if typ.Kind() != reflect.Ptr { if typ.Kind() != reflect.Ptr {
@ -202,6 +201,8 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
func Register(controller BaseController, bindValues []interface{}, func Register(controller BaseController, bindValues []interface{},
registerFunc RegisterFunc) error { registerFunc RegisterFunc) error {
CallOnActivate(controller, &bindValues, registerFunc)
t, err := ActivateController(controller, bindValues) t, err := ActivateController(controller, bindValues)
if err != nil { if err != nil {
return err return err

View File

@ -7,6 +7,7 @@ import (
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/mvc" "github.com/kataras/iris/mvc"
"github.com/kataras/iris/mvc/activator"
"github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router"
"github.com/kataras/iris/httptest" "github.com/kataras/iris/httptest"
@ -493,3 +494,33 @@ func TestControllerRelPathFromFunc(t *testing.T) {
e.GET("/anything/here").Expect().Status(httptest.StatusOK). e.GET("/anything/here").Expect().Status(httptest.StatusOK).
Body().Equal("GET:/anything/here") Body().Equal("GET:/anything/here")
} }
type testControllerActivateListener struct {
mvc.Controller
TitlePointer *testBindType
}
func (c *testControllerActivateListener) OnActivate(p *activator.ActivatePayload) {
p.EnsureBindValue(&testBindType{
title: "default title",
})
}
func (c *testControllerActivateListener) Get() {
c.Text = c.TitlePointer.title
}
func TestControllerActivateListener(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerActivateListener))
app.Controller("/manual", new(testControllerActivateListener), &testBindType{
title: "my title",
})
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
Body().Equal("default title")
e.GET("/manual").Expect().Status(httptest.StatusOK).
Body().Equal("my title")
}

19
mvc/go19.go Normal file
View File

@ -0,0 +1,19 @@
// +build go1.9
package mvc
import (
"github.com/kataras/iris/mvc/activator"
)
// ActivatePayload contains the necessary information and the ability
// to alt a controller's registration options, i.e the binder.
//
// With `ActivatePayload` the `Controller` can register custom routes
// or modify the provided values that will be binded to the
// controller later on.
//
// Look the `mvc/activator#ActivatePayload` for its implementation.
//
// A shortcut for the `mvc/activator#ActivatePayload`, useful when `OnActivate` is being used.
type ActivatePayload = activator.ActivatePayload

View File

@ -2,9 +2,14 @@ package mvc
import ( import (
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator"
"github.com/kataras/iris/sessions" "github.com/kataras/iris/sessions"
"github.com/kataras/golog"
) )
var defaultManager = sessions.New(sessions.Config{})
// SessionController is a simple `Controller` implementation // SessionController is a simple `Controller` implementation
// which requires a binded session manager in order to give // which requires a binded session manager in order to give
// direct access to the current client's session via its `Session` field. // direct access to the current client's session via its `Session` field.
@ -15,21 +20,27 @@ type SessionController struct {
Session *sessions.Session Session *sessions.Session
} }
var managerMissing = "MVC SessionController: session manager field is nil, you have to bind it to a *sessions.Sessions" // OnActivate called, once per application lifecycle NOT request,
// every single time the dev registers a specific SessionController-based controller.
// It makes sure that its "Manager" field is filled
// even if the caller didn't provide any sessions manager via the `app.Controller` function.
func (s *SessionController) OnActivate(p *activator.ActivatePayload) {
if p.EnsureBindValue(defaultManager) {
golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field,
therefore this controller is using the default sessions manager instead.
Please refer to the documentation to learn how you can provide the session manager`)
}
}
// BeginRequest calls the Controller's BeginRequest // BeginRequest calls the Controller's BeginRequest
// and tries to initialize the current user's Session. // and tries to initialize the current user's Session.
func (s *SessionController) BeginRequest(ctx context.Context) { func (s *SessionController) BeginRequest(ctx context.Context) {
s.Controller.BeginRequest(ctx) s.Controller.BeginRequest(ctx)
if s.Manager == nil { if s.Manager == nil {
ctx.Application().Logger().Errorf(managerMissing) ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug
because the SessionController should predict this on its activation state and use a default one automatically`)
return return
} }
s.Session = s.Manager.Start(ctx) s.Session = s.Manager.Start(ctx)
} }
/* TODO:
Maybe add struct tags on `binder` for required binded values
in order to log error if some of the bindings are missing or leave that to the end-developers?
*/

View File

@ -22,7 +22,7 @@ type Database struct {
// Service is the underline badger database connection, // Service is the underline badger database connection,
// it's initialized at `New` or `NewFromDB`. // it's initialized at `New` or `NewFromDB`.
// Can be used to get stats. // Can be used to get stats.
Service *badger.KV Service *badger.DB
async bool async bool
} }
@ -51,7 +51,7 @@ func New(directoryPath string) (*Database, error) {
opts.Dir = directoryPath opts.Dir = directoryPath
opts.ValueDir = directoryPath opts.ValueDir = directoryPath
service, err := badger.NewKV(&opts) service, err := badger.Open(&opts)
if err != nil { if err != nil {
golog.Errorf("unable to initialize the badger-based session database: %v", err) golog.Errorf("unable to initialize the badger-based session database: %v", err)
@ -62,7 +62,7 @@ func New(directoryPath string) (*Database, error) {
} }
// NewFromDB same as `New` but accepts an already-created custom badger connection instead. // NewFromDB same as `New` but accepts an already-created custom badger connection instead.
func NewFromDB(service *badger.KV) (*Database, error) { func NewFromDB(service *badger.DB) (*Database, error) {
if service == nil { if service == nil {
return nil, errors.New("underline database is missing") return nil, errors.New("underline database is missing")
} }
@ -75,30 +75,37 @@ func NewFromDB(service *badger.KV) (*Database, error) {
// Cleanup removes any invalid(have expired) session entries, // Cleanup removes any invalid(have expired) session entries,
// it's being called automatically on `New` as well. // it's being called automatically on `New` as well.
func (db *Database) Cleanup() error { func (db *Database) Cleanup() (err error) {
rep := errors.NewReporter() rep := errors.NewReporter()
iter := db.Service.NewIterator(badger.DefaultIteratorOptions) txn := db.Service.NewTransaction(true)
defer txn.Commit(nil)
iter := txn.NewIterator(badger.DefaultIteratorOptions)
defer iter.Close()
for iter.Rewind(); iter.Valid(); iter.Next() { for iter.Rewind(); iter.Valid(); iter.Next() {
// Remember that the contents of the returned slice should not be modified, and // Remember that the contents of the returned slice should not be modified, and
// only valid until the next call to Next. // only valid until the next call to Next.
item := iter.Item() item := iter.Item()
err := item.Value(func(b []byte) error { b, err := item.Value()
if rep.AddErr(err) {
continue
}
storeDB, err := sessions.DecodeRemoteStore(b) storeDB, err := sessions.DecodeRemoteStore(b)
if err != nil { if rep.AddErr(err) {
return err continue
} }
if storeDB.Lifetime.HasExpired() { if storeDB.Lifetime.HasExpired() {
err = db.Service.Delete(item.Key()) if err := txn.Delete(item.Key()); err != nil {
}
return err
})
rep.AddErr(err) rep.AddErr(err)
} }
}
}
iter.Close()
return rep.Return() return rep.Return()
} }
@ -112,21 +119,28 @@ func (db *Database) Async(useGoRoutines bool) *Database {
// Load loads the sessions from the badger(key-value file-based) session storage. // Load loads the sessions from the badger(key-value file-based) session storage.
func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) { func (db *Database) Load(sid string) (storeDB sessions.RemoteStore) {
bsid := []byte(sid) bsid := []byte(sid)
iter := db.Service.NewIterator(badger.DefaultIteratorOptions)
defer iter.Close()
iter.Seek(bsid) txn := db.Service.NewTransaction(false)
if !iter.Valid() { defer txn.Discard()
item, err := txn.Get(bsid)
if err != nil {
// Key not found, don't report this, session manager will create a new session as it should.
return return
} }
item := iter.Item()
item.Value(func(b []byte) (err error) { b, err := item.Value()
if err != nil {
golog.Errorf("error while trying to get the serialized session(%s) from the remote store: %v", sid, err)
return
}
storeDB, err = sessions.DecodeRemoteStore(b) // decode the whole value, as a remote store storeDB, err = sessions.DecodeRemoteStore(b) // decode the whole value, as a remote store
if err != nil { if err != nil {
golog.Errorf("error while trying to load from the remote store: %v", err) golog.Errorf("error while trying to load from the remote store: %v", err)
} }
return
})
return return
} }
@ -155,19 +169,28 @@ func (db *Database) sync(p sessions.SyncPayload) {
golog.Errorf("error while serializing the remote store: %v", err) golog.Errorf("error while serializing the remote store: %v", err)
} }
// err = db.Service.Set(bsid, s, meta) txn := db.Service.NewTransaction(true)
e := &badger.Entry{
Key: bsid, err = txn.Set(bsid, s, 0x00)
Value: s,
}
err = db.Service.BatchSet([]*badger.Entry{e})
if err != nil { if err != nil {
golog.Errorf("error while writing the session(%s) to the database: %v", p.SessionID, err) txn.Discard()
golog.Errorf("error while trying to save the session(%s) to the database: %v", p.SessionID, err)
return
}
if err := txn.Commit(nil); err != nil { // Commit will call the Discard automatically.
golog.Errorf("error while committing the session(%s) changes to the database: %v", p.SessionID, err)
} }
} }
func (db *Database) destroy(bsid []byte) error { func (db *Database) destroy(bsid []byte) error {
return db.Service.Delete(bsid) txn := db.Service.NewTransaction(true)
err := txn.Delete(bsid)
if err != nil {
return err
}
return txn.Commit(nil)
} }
// Close shutdowns the badger connection. // Close shutdowns the badger connection.