Browse Source

session: fork Macaron mysql session middleware

This changes forks the mysql part of the Macaron session middleware.

In the forked mysql file:

- takes in a config setting for SetConnMaxLifetime (this solves wait_timeout
problem if it is set to a shorter interval than wait_timeout)
- removes the panic when an error is returned in the Exist function.
- retries the exist query once
- retries the GC query once
Daniel Lee 7 năm trước cách đây
mục cha
commit
3ca1e06509

+ 1 - 1
pkg/api/common_test.go

@@ -99,7 +99,7 @@ func setupScenarioContext(url string) *scenarioContext {
 	}))
 
 	sc.m.Use(middleware.GetContextHandler())
-	sc.m.Use(middleware.Sessioner(&session.Options{}))
+	sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
 
 	return sc
 }

+ 1 - 1
pkg/api/http_server.go

@@ -175,7 +175,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
 	m.Use(hs.healthHandler)
 	m.Use(hs.metricsEndpoint)
 	m.Use(middleware.GetContextHandler())
-	m.Use(middleware.Sessioner(&setting.SessionOptions))
+	m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
 	m.Use(middleware.OrgRedirect())
 
 	// needs to be after context handler

+ 1 - 1
pkg/middleware/middleware_test.go

@@ -338,7 +338,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 		sc.m.Use(GetContextHandler())
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}))
+		sc.m.Use(Sessioner(&ms.Options{}, 0))
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 

+ 1 - 1
pkg/middleware/recovery_test.go

@@ -63,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 		sc.m.Use(GetContextHandler())
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}))
+		sc.m.Use(Sessioner(&ms.Options{}, 0))
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 

+ 2 - 2
pkg/middleware/session.go

@@ -8,8 +8,8 @@ import (
 	"github.com/grafana/grafana/pkg/services/session"
 )
 
-func Sessioner(options *ms.Options) macaron.Handler {
-	session.Init(options)
+func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler {
+	session.Init(options, sessionConnMaxLifetime)
 
 	return func(ctx *m.ReqContext) {
 		ctx.Next()

+ 218 - 0
pkg/services/session/mysql.go

@@ -0,0 +1,218 @@
+// Copyright 2013 Beego Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package session
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+	"sync"
+	"time"
+
+	_ "github.com/go-sql-driver/mysql"
+
+	"github.com/go-macaron/session"
+)
+
+// MysqlStore represents a mysql session store implementation.
+type MysqlStore struct {
+	c    *sql.DB
+	sid  string
+	lock sync.RWMutex
+	data map[interface{}]interface{}
+}
+
+// NewMysqlStore creates and returns a mysql session store.
+func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore {
+	return &MysqlStore{
+		c:    c,
+		sid:  sid,
+		data: kv,
+	}
+}
+
+// Set sets value to given key in session.
+func (s *MysqlStore) Set(key, val interface{}) error {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.data[key] = val
+	return nil
+}
+
+// Get gets value by given key in session.
+func (s *MysqlStore) Get(key interface{}) interface{} {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+
+	return s.data[key]
+}
+
+// Delete delete a key from session.
+func (s *MysqlStore) Delete(key interface{}) error {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	delete(s.data, key)
+	return nil
+}
+
+// ID returns current session ID.
+func (s *MysqlStore) ID() string {
+	return s.sid
+}
+
+// Release releases resource and save data to provider.
+func (s *MysqlStore) Release() error {
+	data, err := session.EncodeGob(s.data)
+	if err != nil {
+		return err
+	}
+
+	_, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?",
+		data, time.Now().Unix(), s.sid)
+	return err
+}
+
+// Flush deletes all session data.
+func (s *MysqlStore) Flush() error {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	s.data = make(map[interface{}]interface{})
+	return nil
+}
+
+// MysqlProvider represents a mysql session provider implementation.
+type MysqlProvider struct {
+	c      *sql.DB
+	expire int64
+}
+
+// Init initializes mysql session provider.
+// connStr: username:password@protocol(address)/dbname?param=value
+func (p *MysqlProvider) Init(expire int64, connStr string) (err error) {
+	p.expire = expire
+
+	p.c, err = sql.Open("mysql", connStr)
+	p.c.SetConnMaxLifetime(time.Second * time.Duration(sessionConnMaxLifetime))
+	if err != nil {
+		return err
+	}
+	return p.c.Ping()
+}
+
+// Read returns raw session store by session ID.
+func (p *MysqlProvider) Read(sid string) (session.RawStore, error) {
+	var data []byte
+	err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
+	if err == sql.ErrNoRows {
+		_, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)",
+			sid, "", time.Now().Unix())
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	var kv map[interface{}]interface{}
+	if len(data) == 0 {
+		kv = make(map[interface{}]interface{})
+	} else {
+		kv, err = session.DecodeGob(data)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return NewMysqlStore(p.c, sid, kv), nil
+}
+
+// Exist returns true if session with given ID exists.
+func (p *MysqlProvider) Exist(sid string) bool {
+	exists, err := p.queryExists(sid)
+
+	if err != nil {
+		exists, err = p.queryExists(sid)
+	}
+
+	if err != nil {
+		log.Printf("session/mysql: error checking if session exists: %v", err)
+		return false
+	}
+
+	return exists
+}
+
+func (p *MysqlProvider) queryExists(sid string) (bool, error) {
+	var data []byte
+	err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
+
+	if err != nil && err != sql.ErrNoRows {
+		return false, err
+	}
+
+	return err != sql.ErrNoRows, nil
+}
+
+// Destory deletes a session by session ID.
+func (p *MysqlProvider) Destory(sid string) error {
+	_, err := p.c.Exec("DELETE FROM session WHERE `key`=?", sid)
+	return err
+}
+
+// Regenerate regenerates a session store from old session ID to new one.
+func (p *MysqlProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) {
+	if p.Exist(sid) {
+		return nil, fmt.Errorf("new sid '%s' already exists", sid)
+	}
+
+	if !p.Exist(oldsid) {
+		if _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)",
+			oldsid, "", time.Now().Unix()); err != nil {
+			return nil, err
+		}
+	}
+
+	if _, err = p.c.Exec("UPDATE session SET `key`=? WHERE `key`=?", sid, oldsid); err != nil {
+		return nil, err
+	}
+
+	return p.Read(sid)
+}
+
+// Count counts and returns number of sessions.
+func (p *MysqlProvider) Count() (total int) {
+	if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil {
+		panic("session/mysql: error counting records: " + err.Error())
+	}
+	return total
+}
+
+// GC calls GC to clean expired sessions.
+func (p *MysqlProvider) GC() {
+	var err error
+	if _, err = p.c.Exec("DELETE FROM session WHERE  expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil {
+		_, err = p.c.Exec("DELETE FROM session WHERE  expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire)
+	}
+
+	if err != nil {
+		log.Printf("session/mysql: error garbage collecting: %v", err)
+	}
+}
+
+func init() {
+	session.Register("mysql", &MysqlProvider{})
+}

+ 3 - 2
pkg/services/session/session.go

@@ -6,7 +6,6 @@ import (
 
 	ms "github.com/go-macaron/session"
 	_ "github.com/go-macaron/session/memcache"
-	_ "github.com/go-macaron/session/mysql"
 	_ "github.com/go-macaron/session/postgres"
 	_ "github.com/go-macaron/session/redis"
 	"github.com/grafana/grafana/pkg/log"
@@ -25,6 +24,7 @@ var sessionOptions *ms.Options
 var StartSessionGC func()
 var GetSessionCount func() int
 var sessionLogger = log.New("session")
+var sessionConnMaxLifetime int64
 
 func init() {
 	StartSessionGC = func() {
@@ -37,9 +37,10 @@ func init() {
 	}
 }
 
-func Init(options *ms.Options) {
+func Init(options *ms.Options, connMaxLifetime int64) {
 	var err error
 	sessionOptions = prepareOptions(options)
+	sessionConnMaxLifetime = connMaxLifetime
 	sessionManager, err = ms.NewManager(options.Provider, *options)
 	if err != nil {
 		panic(err)

+ 4 - 1
pkg/setting/setting.go

@@ -131,7 +131,8 @@ var (
 	PluginAppsSkipVerifyTLS bool
 
 	// Session settings.
-	SessionOptions session.Options
+	SessionOptions         session.Options
+	SessionConnMaxLifetime int64
 
 	// Global setting objects.
 	Cfg          *ini.File
@@ -634,6 +635,8 @@ func readSessionConfig() {
 	if SessionOptions.CookiePath == "" {
 		SessionOptions.CookiePath = "/"
 	}
+
+	SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(0)
 }
 
 func initLogging() {