Skip to content
This repository has been archived by the owner on May 1, 2020. It is now read-only.

Commit

Permalink
Fix session restoration
Browse files Browse the repository at this point in the history
Fix broken session restoration by using the session info parser
during session deserialization (from a file or a database).
Also use the parser when receiving the session on the client.
  • Loading branch information
romshark committed Apr 9, 2018
1 parent 2b6da64 commit 9b74216
Show file tree
Hide file tree
Showing 18 changed files with 231 additions and 90 deletions.
6 changes: 1 addition & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,7 @@ func (clt *Client) notifySessionCreated(newSession *Session) error {
}
}

encoded, err := json.Marshal(struct {
Key string `json:"k"`
Creation time.Time `json:"c"`
Info map[string]interface{} `json:"i"`
}{
encoded, err := json.Marshal(JSONEncodedSession{
newSession.Key,
newSession.Creation,
sessionInfo,
Expand Down
3 changes: 2 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const (
type Client struct {
serverAddr string
impl Implementation
sessionInfoParser SessionInfoParser
sessionInfoParser webwire.SessionInfoParser
status Status
defaultReqTimeout time.Duration
reconnInterval time.Duration
Expand Down Expand Up @@ -279,6 +279,7 @@ func (clt *Client) RestoreSession(sessionKey []byte) error {
if err != nil {
return err
}

clt.sessionLock.Lock()
clt.session = restoredSession
clt.sessionLock.Unlock()
Expand Down
8 changes: 1 addition & 7 deletions client/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ package client

import (
"encoding/json"
"time"

webwire "github.com/qbeon/webwire-go"
)

func (clt *Client) handleSessionCreated(msgPayload webwire.Payload) {
var encoded struct {
Key string `json:"k"`
Creation time.Time `json:"c"`
Info map[string]interface{} `json:"i"`
}

var encoded webwire.JSONEncodedSession
if err := json.Unmarshal(msgPayload.Data, &encoded); err != nil {
clt.errorLog.Printf("Failed unmarshalling session object: %s", err)
return
Expand Down
4 changes: 0 additions & 4 deletions client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,3 @@ type Implementation interface {
// either by the server or the client itself
OnSessionClosed()
}

// SessionInfoParser is invoked during the parsing of a newly assigned
// session, it must return a webwire.SessionInfo compliant object
type SessionInfoParser func(data map[string]interface{}) webwire.SessionInfo
4 changes: 2 additions & 2 deletions client/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
// Options represents the options used during the creation a new client instance
type Options struct {
// SessionInfoParser defines the optional session info parser function
SessionInfoParser func(map[string]interface{}) webwire.SessionInfo
SessionInfoParser webwire.SessionInfoParser

// DefaultRequestTimeout defines the default request timeout duration
// used by client.Request and client.RestoreSession
Expand Down Expand Up @@ -57,7 +57,7 @@ type Options struct {
// SetDefaults sets default values for undefined required options
func (opts *Options) SetDefaults() {
if opts.SessionInfoParser == nil {
opts.SessionInfoParser = GenericSessionInfoParser
opts.SessionInfoParser = webwire.GenericSessionInfoParser
}

if opts.DefaultRequestTimeout < 1 {
Expand Down
22 changes: 18 additions & 4 deletions client/requestSessionRestoration.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
// requestSessionRestoration sends a session restoration request
// and decodes the session object from the received reply.
// Expects the client to be connected beforehand
func (clt *Client) requestSessionRestoration(sessionKey []byte) (*webwire.Session, error) {
func (clt *Client) requestSessionRestoration(sessionKey []byte) (
*webwire.Session,
error,
) {
reply, err := clt.sendNamelessRequest(
webwire.MsgRestoreSession,
webwire.Payload{
Expand All @@ -23,14 +26,25 @@ func (clt *Client) requestSessionRestoration(sessionKey []byte) (*webwire.Sessio
return nil, err
}

var session webwire.Session
if err := json.Unmarshal(reply.Data, &session); err != nil {
// Unmarshal JSON encoded session object
var encodedSessionObj webwire.JSONEncodedSession
if err := json.Unmarshal(reply.Data, &encodedSessionObj); err != nil {
return nil, fmt.Errorf(
"Couldn't unmarshal restored session from reply('%s'): %s",
string(reply.Data),
err,
)
}

return &session, nil
// Parse session info object
var decodedInfo webwire.SessionInfo
if encodedSessionObj.Info != nil {
decodedInfo = clt.sessionInfoParser(encodedSessionObj.Info)
}

return &webwire.Session{
Key: encodedSessionObj.Key,
Creation: encodedSessionObj.Creation,
Info: decodedInfo,
}, nil
}
31 changes: 19 additions & 12 deletions defaultSessionManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (

// SessionFile represents the serialization structure of a default session file
type SessionFile struct {
Creation time.Time `json:"c"`
Info SessionInfo `json:"i"`
Creation time.Time `json:"c"`
Info map[string]interface{} `json:"i"`
}

// Parse parses the session file from a file
Expand Down Expand Up @@ -94,32 +94,39 @@ func (mng *DefaultSessionManager) OnSessionCreated(client *Client) error {
sess := client.Session()
sessFile := SessionFile{
Creation: sess.Creation,
Info: sess.Info,
Info: SessionInfoToVarMap(sess.Info),
}
return sessFile.WriteFile(mng.filePath(client.SessionKey()))
}

// OnSessionLookup implements the session manager interface.
// It searches the session file directory for the session file and loads it
func (mng *DefaultSessionManager) OnSessionLookup(key string) (*Session, error) {
func (mng *DefaultSessionManager) OnSessionLookup(key string) (
bool,
time.Time,
map[string]interface{},
error,
) {
path := mng.filePath(key)
_, err := os.Stat(path)
if os.IsNotExist(err) {
return nil, nil
return false, time.Time{}, nil, nil
} else if err != nil {
return nil, fmt.Errorf("Unexpected error during file lookup: %s", err)
return false, time.Time{}, nil, fmt.Errorf(
"Unexpected error during file lookup: %s",
err,
)
}

var file SessionFile
if err := file.Parse(path); err != nil {
return nil, fmt.Errorf("Couldn't parse session file: %s", err)
return false, time.Time{}, nil, fmt.Errorf(
"Couldn't parse session file: %s",
err,
)
}

return &Session{
Key: key,
Creation: file.Creation,
Info: file.Info,
}, nil
return true, file.Creation, file.Info, nil
}

// OnSessionClosed implements the session manager interface.
Expand Down
8 changes: 3 additions & 5 deletions client/genericSessionInfo.go → genericSessionInfo.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package client
package webwire

import (
"reflect"

webwire "github.com/qbeon/webwire-go"
)

func deepCopy(src interface{}) interface{} {
Expand Down Expand Up @@ -81,7 +79,7 @@ type GenericSessionInfo struct {

// Copy implements the webwire.SessionInfo interface.
// It deep-copies the object and returns it's exact clone
func (sinf *GenericSessionInfo) Copy() webwire.SessionInfo {
func (sinf *GenericSessionInfo) Copy() SessionInfo {
return &GenericSessionInfo{
data: deepCopy(sinf.data).(map[string]interface{}),
}
Expand Down Expand Up @@ -117,6 +115,6 @@ func (sinf *GenericSessionInfo) Value(fieldName string) interface{} {
// GenericSessionInfoParser represents a default implementation of a
// session info object parser. It parses the info object into a generic
// session info type implementing the webwire.SessionInfo interface
func GenericSessionInfoParser(data map[string]interface{}) webwire.SessionInfo {
func GenericSessionInfoParser(data map[string]interface{}) SessionInfo {
return &GenericSessionInfo{data}
}
47 changes: 32 additions & 15 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package webwire
import (
"context"
"net/http"
"time"
)

// ServerImplementation defines the interface of a webwire server implementation
Expand Down Expand Up @@ -61,25 +62,33 @@ type ServerImplementation interface {
type SessionManager interface {
// OnSessionCreated is invoked after the synchronization of the new session
// to the remote client.
// The actual created session can be retrieved from the provided client agent.
// The actual created session is retrieved from the provided client agent.
// If OnSessionCreated returns an error then this error is logged
// but the session will not be destroyed and will remain active!
// The only consequence of OnSessionCreation failing is that the server won't be able
// to restore the session after the client is disconnected.
// The only consequence of OnSessionCreation failing is that the server
// won't be able to restore the session after the client is disconnected.
//
// This hook will be invoked by the goroutine calling the client.CreateSession
// client agent method
// This hook will be invoked by the goroutine calling the
// client.CreateSession client agent method
OnSessionCreated(client *Client) error

// OnSessionLookup is invoked when the server is looking for a specific session given its key.
// It must return the exact copy of the session object associated with the given key
// for sessions to be properly restorable. If no session is found it must return nil
// instead of the session and must not return any error.
// If an error is returned then the it'll be logged and the session restoration will fail.
// OnSessionLookup is invoked when the server is looking for a specific
// session given its key.
// If the session was found it must return true, the time of its creation
// and the exact copy of the session info object. Otherwise false must be
// returned and all other return fields can be left empty.
//
// This hook will be invoked by the goroutine serving the associated client and will block any
// other interactions with this client while executing
OnSessionLookup(key string) (*Session, error)
// If an error is returned then it'll be logged and the session restoration
// will fail. An error should not be returned when the session wasn't found.
//
// This hook will be invoked by the goroutine serving the associated client
// and will block any other interactions with this client while executing
OnSessionLookup(key string) (
exists bool,
creation time.Time,
info map[string]interface{},
err error,
)

// OnSessionClosed is invoked when the active session of the given client
// is closed (thus destroyed) either by the server or the client through a
Expand All @@ -89,8 +98,9 @@ type SessionManager interface {
// If an error is returned then the it is logged.
//
// This hook is invoked by either a goroutine calling the client.CloseSession()
// client agent method, or the goroutine serving the associated client, in the case of which
// it will block any other interactions with this client while executing
// client agent method, or the goroutine serving the associated client,
// in the case of which it will block any other interactions with
// this client while executing
OnSessionClosed(client *Client) error
}

Expand Down Expand Up @@ -128,3 +138,10 @@ type SessionInfo interface {
// race conditions and undefined behavior
Copy() SessionInfo
}

// SessionInfoParser represents the type of a session info parser function.
// The session info parser is invoked during the parsing of a newly assigned
// session on the client, as well as during the parsing of a saved serialized
// session. It must return a webwire.SessionInfo compliant object constructed
// from the data given
type SessionInfoParser func(map[string]interface{}) SessionInfo
5 changes: 5 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type ServerOptions struct {
SessionsEnabled bool
SessionManager SessionManager
SessionKeyGenerator SessionKeyGenerator
SessionInfoParser SessionInfoParser
MaxSessionConnections uint
WarnLog io.Writer
ErrorLog io.Writer
Expand All @@ -26,6 +27,10 @@ func (srvOpt *ServerOptions) SetDefaults() {
srvOpt.SessionKeyGenerator = NewDefaultSessionKeyGenerator()
}

if srvOpt.SessionInfoParser == nil {
srvOpt.SessionInfoParser = GenericSessionInfoParser
}

if srvOpt.WarnLog == nil {
srvOpt.WarnLog = os.Stdout
}
Expand Down
50 changes: 37 additions & 13 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ const protocolVersion = "1.3"
// Server represents a headless WebWire server instance,
// where headless means there's no HTTP server that's hosting it
type Server struct {
impl ServerImplementation
sessionManager SessionManager
sessionKeyGen SessionKeyGenerator
impl ServerImplementation
sessionManager SessionManager
sessionKeyGen SessionKeyGenerator
sessionInfoParser SessionInfoParser

// State
shutdown bool
Expand All @@ -43,9 +44,10 @@ func NewServer(implementation ServerImplementation, opts ServerOptions) *Server
opts.SetDefaults()

srv := Server{
impl: implementation,
sessionManager: opts.SessionManager,
sessionKeyGen: opts.SessionKeyGenerator,
impl: implementation,
sessionManager: opts.SessionManager,
sessionKeyGen: opts.SessionKeyGenerator,
sessionInfoParser: opts.SessionInfoParser,

// State
shutdown: false,
Expand Down Expand Up @@ -90,28 +92,50 @@ func (srv *Server) handleSessionRestore(clt *Client, msg *Message) error {
return nil
}

session, err := srv.sessionManager.OnSessionLookup(key)
//session, err := srv.sessionManager.OnSessionLookup(key)
exists, creation, info, err := srv.sessionManager.OnSessionLookup(key)
if err != nil {
// TODO: return internal server error and log it
srv.failMsg(clt, msg, nil)
return fmt.Errorf("CRITICAL: Session search handler failed: %s", err)
}
if session == nil {
if !exists {
srv.failMsg(clt, msg, SessNotFoundErr{})
return nil
}

encodedSessionObj := JSONEncodedSession{
Key: key,
Creation: creation,
Info: info,
}

// JSON encode the session
encodedSession, err := json.Marshal(session)
encodedSession, err := json.Marshal(&encodedSessionObj)
if err != nil {
// TODO: return internal server error and log it
srv.failMsg(clt, msg, nil)
return fmt.Errorf("Couldn't encode session object (%v): %s", session, err)
return fmt.Errorf(
"Couldn't encode session object (%v): %s",
encodedSessionObj,
err,
)
}

// parse attached session info
var parsedSessInfo SessionInfo
if info != nil && srv.sessionInfoParser != nil {
parsedSessInfo = srv.sessionInfoParser(info)
}

clt.setSession(session)
clt.setSession(&Session{
Key: key,
Creation: creation,
Info: parsedSessInfo,
})
if okay := srv.sessionRegistry.register(clt); !okay {
panic(fmt.Errorf("The number of concurrent session connections was unexpectedly exceeded"))
panic(fmt.Errorf("The number of concurrent session connections was " +
"unexpectedly exceeded",
))
}

srv.fulfillMsg(clt, msg, Payload{
Expand Down

1 comment on commit 9b74216

@romshark
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SessionManager interface method OnSessionLookup changed, it now expects the data components of a session object rather than an actual session object in order to use the parser on the session info field.

Please sign in to comment.