Refactoring to clean up API

This commit is contained in:
Chad Retz 2019-02-24 23:14:26 -06:00
parent 5fa024d1e3
commit 5120875087
12 changed files with 251 additions and 222 deletions

View File

@ -8,7 +8,7 @@ import (
) )
type Gun struct { type Gun struct {
peers []*gunPeer peers []*Peer
storage Storage storage Storage
soulGen func() string soulGen func() string
peerErrorHandler func(*ErrPeer) peerErrorHandler func(*ErrPeer)
@ -42,7 +42,7 @@ const DefaultPeerSleepOnError = 30 * time.Second
func New(ctx context.Context, config Config) (*Gun, error) { func New(ctx context.Context, config Config) (*Gun, error) {
g := &Gun{ g := &Gun{
peers: make([]*gunPeer, len(config.PeerURLs)), peers: make([]*Peer, len(config.PeerURLs)),
storage: config.Storage, storage: config.Storage,
soulGen: config.SoulGen, soulGen: config.SoulGen,
peerErrorHandler: config.PeerErrorHandler, peerErrorHandler: config.PeerErrorHandler,
@ -59,8 +59,8 @@ func New(ctx context.Context, config Config) (*Gun, error) {
var err error var err error
for i := 0; i < len(config.PeerURLs) && err == nil; i++ { for i := 0; i < len(config.PeerURLs) && err == nil; i++ {
peerURL := config.PeerURLs[i] peerURL := config.PeerURLs[i]
connPeer := func() (Peer, error) { return NewPeer(ctx, peerURL) } newConn := func() (PeerConn, error) { return NewPeerConn(ctx, peerURL) }
if g.peers[i], err = newGunPeer(peerURL, connPeer, sleepOnError); err != nil { if g.peers[i], err = newPeer(peerURL, newConn, sleepOnError); err != nil {
err = fmt.Errorf("Failed connecting to peer %v: %v", peerURL, err) err = fmt.Errorf("Failed connecting to peer %v: %v", peerURL, err)
} }
} }
@ -88,6 +88,14 @@ func New(ctx context.Context, config Config) (*Gun, error) {
return g, nil return g, nil
} }
func (g *Gun) Scoped(ctx context.Context, key string, children ...string) *Scoped {
s := newScoped(g, nil, key)
if len(children) > 0 {
s = s.Scoped(ctx, children[0], children[1:]...)
}
return s
}
func (g *Gun) Close() error { func (g *Gun) Close() error {
var errs []error var errs []error
for _, p := range g.peers { for _, p := range g.peers {
@ -104,11 +112,7 @@ func (g *Gun) Close() error {
} }
} }
func (g *Gun) Send(ctx context.Context, msg *Message) <-chan *ErrPeer { func (g *Gun) send(ctx context.Context, msg *Message, ignorePeer *Peer) <-chan *ErrPeer {
return g.send(ctx, msg, nil)
}
func (g *Gun) send(ctx context.Context, msg *Message, ignorePeer *gunPeer) <-chan *ErrPeer {
ch := make(chan *ErrPeer, len(g.peers)) ch := make(chan *ErrPeer, len(g.peers))
// Everything async // Everything async
go func() { go func() {
@ -119,7 +123,7 @@ func (g *Gun) send(ctx context.Context, msg *Message, ignorePeer *gunPeer) <-cha
continue continue
} }
wg.Add(1) wg.Add(1)
go func(peer *gunPeer) { go func(peer *Peer) {
defer wg.Done() defer wg.Done()
// Just do nothing if the peer is bad and we couldn't send // Just do nothing if the peer is bad and we couldn't send
if _, err := peer.send(ctx, msg); err != nil { if _, err := peer.send(ctx, msg); err != nil {
@ -136,11 +140,11 @@ func (g *Gun) send(ctx context.Context, msg *Message, ignorePeer *gunPeer) <-cha
func (g *Gun) startReceiving() { func (g *Gun) startReceiving() {
for _, peer := range g.peers { for _, peer := range g.peers {
go func(peer *gunPeer) { go func(peer *Peer) {
// TDO: some kind of overall context is probably needed // TDO: some kind of overall context is probably needed
ctx, cancelFn := context.WithCancel(context.TODO()) ctx, cancelFn := context.WithCancel(context.TODO())
defer cancelFn() defer cancelFn()
for !peer.closed() { for !peer.Closed() {
// We might not be able receive because peer is sleeping from // We might not be able receive because peer is sleeping from
// an error happened within or a just-before send error. // an error happened within or a just-before send error.
if ok, msgs, err := peer.receive(ctx); !ok { if ok, msgs, err := peer.receive(ctx); !ok {
@ -152,7 +156,7 @@ func (g *Gun) startReceiving() {
} else { } else {
// Go over each message and see if it needs delivering or rebroadcasting // Go over each message and see if it needs delivering or rebroadcasting
for _, msg := range msgs { for _, msg := range msgs {
g.onPeerMessage(ctx, &MessageReceived{Message: msg, peer: peer}) g.onPeerMessage(ctx, &MessageReceived{Message: msg, Peer: peer})
} }
} }
} }
@ -176,17 +180,17 @@ func (g *Gun) onPeerMessage(ctx context.Context, msg *MessageReceived) {
if msg.PID == "" { if msg.PID == "" {
// This is a request, set the PID and send it back // This is a request, set the PID and send it back
msg.PID = g.myPeerID msg.PID = g.myPeerID
if _, err := msg.peer.send(ctx, msg.Message); err != nil { if _, err := msg.Peer.send(ctx, msg.Message); err != nil {
go g.onPeerError(&ErrPeer{err, msg.peer}) go g.onPeerError(&ErrPeer{err, msg.Peer})
} }
} else { } else {
// This is them telling us theirs // This is them telling us theirs
msg.peer.id = msg.PID msg.Peer.id = msg.PID
} }
return return
} }
// Unhandled message means rebroadcast // Unhandled message means rebroadcast
g.send(ctx, msg.Message, msg.peer) g.send(ctx, msg.Message, msg.Peer)
} }
func (g *Gun) onPeerError(err *ErrPeer) { func (g *Gun) onPeerError(err *ErrPeer) {
@ -195,26 +199,18 @@ func (g *Gun) onPeerError(err *ErrPeer) {
} }
} }
func (g *Gun) RegisterMessageIDListener(id string, ch chan<- *MessageReceived) { func (g *Gun) registerMessageIDListener(id string, ch chan<- *MessageReceived) {
g.messageIDListenersLock.Lock() g.messageIDListenersLock.Lock()
defer g.messageIDListenersLock.Unlock() defer g.messageIDListenersLock.Unlock()
g.messageIDListeners[id] = ch g.messageIDListeners[id] = ch
} }
func (g *Gun) UnregisterMessageIDListener(id string) { func (g *Gun) unregisterMessageIDListener(id string) {
g.messageIDListenersLock.Lock() g.messageIDListenersLock.Lock()
defer g.messageIDListenersLock.Unlock() defer g.messageIDListenersLock.Unlock()
delete(g.messageIDListeners, id) delete(g.messageIDListeners, id)
} }
func (g *Gun) Scoped(ctx context.Context, key string, children ...string) *Scoped {
s := newScoped(g, nil, key)
if len(children) > 0 {
s = s.Scoped(ctx, children[0], children[1:]...)
}
return s
}
func safeReceivedMessageSend(ch chan<- *MessageReceived, msg *MessageReceived) { func safeReceivedMessageSend(ch chan<- *MessageReceived, msg *MessageReceived) {
// Due to the fact that we may send on a closed channel here, we ignore the panic // Due to the fact that we may send on a closed channel here, we ignore the panic
defer func() { recover() }() defer func() { recover() }()

View File

@ -1,111 +0,0 @@
package gun
import (
"context"
"sync"
"time"
)
type gunPeer struct {
url string
connPeer func() (Peer, error)
sleepOnErr time.Duration // TODO: would be better as backoff
id string
peer Peer
peerBad bool // If true, don't try anything
peerLock sync.Mutex
}
func newGunPeer(url string, connPeer func() (Peer, error), sleepOnErr time.Duration) (*gunPeer, error) {
p := &gunPeer{url: url, connPeer: connPeer, sleepOnErr: sleepOnErr}
var err error
if p.peer, err = connPeer(); err != nil {
return nil, err
}
return p, nil
}
func (g *gunPeer) ID() string { return g.id }
func (g *gunPeer) reconnectPeer() (err error) {
g.peerLock.Lock()
defer g.peerLock.Unlock()
if g.peer == nil && g.peerBad {
g.peerBad = false
if g.peer, err = g.connPeer(); err != nil {
g.peerBad = true
time.AfterFunc(g.sleepOnErr, func() { g.reconnectPeer() })
}
}
return
}
// Can be nil peer if currently bad
func (g *gunPeer) connectedPeer() Peer {
g.peerLock.Lock()
defer g.peerLock.Unlock()
return g.peer
}
func (g *gunPeer) markPeerErrored(p Peer) {
g.peerLock.Lock()
defer g.peerLock.Unlock()
if p == g.peer {
g.peer = nil
g.peerBad = true
p.Close()
time.AfterFunc(g.sleepOnErr, func() { g.reconnectPeer() })
}
}
func (g *gunPeer) send(ctx context.Context, msg *Message, moreMsgs ...*Message) (ok bool, err error) {
p := g.connectedPeer()
if p == nil {
return false, nil
}
// Clone them with peer "to"
updatedMsg := msg.Clone()
updatedMsg.To = g.url
updatedMoreMsgs := make([]*Message, len(moreMsgs))
for i, moreMsg := range moreMsgs {
moreMsg := moreMsg.Clone()
moreMsg.To = g.url
updatedMoreMsgs[i] = moreMsg
}
if err = p.Send(ctx, updatedMsg, updatedMoreMsgs...); err != nil {
g.markPeerErrored(p)
return false, err
} else {
return true, nil
}
}
func (g *gunPeer) receive(ctx context.Context) (ok bool, msgs []*Message, err error) {
if p := g.connectedPeer(); p == nil {
return false, nil, nil
} else if msgs, err = p.Receive(ctx); err != nil {
g.markPeerErrored(p)
return false, nil, err
} else {
return true, msgs, nil
}
}
func (g *gunPeer) Close() error {
g.peerLock.Lock()
defer g.peerLock.Unlock()
var err error
if g.peer != nil {
err = g.peer.Close()
g.peer = nil
}
g.peerBad = false
return err
}
func (g *gunPeer) closed() bool {
g.peerLock.Lock()
defer g.peerLock.Unlock()
return g.peer == nil && !g.peerBad
}

View File

@ -16,12 +16,6 @@ type Message struct {
Err string `json:"err,omitempty"` Err string `json:"err,omitempty"`
} }
func (m *Message) Clone() *Message {
msg := &Message{}
*msg = *m
return msg
}
type MessageGetRequest struct { type MessageGetRequest struct {
Soul string `json:"#,omitempty"` Soul string `json:"#,omitempty"`
Field string `json:".,omitempty"` Field string `json:".,omitempty"`
@ -29,5 +23,5 @@ type MessageGetRequest struct {
type MessageReceived struct { type MessageReceived struct {
*Message *Message
peer *gunPeer Peer *Peer
} }

View File

@ -8,7 +8,7 @@ import (
) )
func DefaultSoulGen() string { func DefaultSoulGen() string {
ms, uniqueNum := TimeNowUniqueUnix() ms, uniqueNum := timeNowUniqueUnix()
s := strconv.FormatInt(ms, 36) s := strconv.FormatInt(ms, 36)
if uniqueNum > 0 { if uniqueNum > 0 {
s += strconv.FormatInt(uniqueNum, 36) s += strconv.FormatInt(uniqueNum, 36)
@ -54,7 +54,7 @@ func (n *Node) UnmarshalJSON(b []byte) error {
} }
} else if val, err := dec.Token(); err != nil { } else if val, err := dec.Token(); err != nil {
return err return err
} else if n.Values[keyStr], err = DecodeJSONValue(val, dec); err != nil { } else if n.Values[keyStr], err = ValueDecodeJSON(val, dec); err != nil {
return err return err
} }
} }
@ -62,14 +62,15 @@ func (n *Node) UnmarshalJSON(b []byte) error {
type Metadata struct { type Metadata struct {
Soul string `json:"#,omitempty"` Soul string `json:"#,omitempty"`
State map[string]int64 `json:">,omitempty"` State map[string]State `json:">,omitempty"`
} }
// TODO: put private methd to seal enum // TODO: put private method to seal enum
type Value interface { type Value interface {
nodeValue()
} }
func DecodeJSONValue(token json.Token, dec *json.Decoder) (Value, error) { func ValueDecodeJSON(token json.Token, dec *json.Decoder) (Value, error) {
switch token := token.(type) { switch token := token.(type) {
case nil: case nil:
return nil, nil return nil, nil
@ -103,16 +104,27 @@ func DecodeJSONValue(token json.Token, dec *json.Decoder) (Value, error) {
} }
type ValueString string type ValueString string
func (ValueString) nodeValue() {}
type ValueNumber string type ValueNumber string
func (ValueNumber) nodeValue() {}
type ValueBool bool type ValueBool bool
func (ValueBool) nodeValue() {}
type ValueRelation string type ValueRelation string
func (ValueRelation) nodeValue() {}
func (n ValueRelation) MarshalJSON() ([]byte, error) { func (n ValueRelation) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{"#": string(n)}) return json.Marshal(map[string]string{"#": string(n)})
} }
type ValueWithState struct { // type ValueWithState struct {
Value Value // Value Value
// This is 0 for top-level values // // This is 0 for top-level values
State int64 // State int64
} // }

View File

@ -6,41 +6,160 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"sync" "sync"
"time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
type ErrPeer struct { type ErrPeer struct {
Err error Err error
peer *gunPeer Peer *Peer
} }
func (e *ErrPeer) Error() string { return fmt.Sprintf("Error on peer %v: %v", e.peer, e.Err) } func (e *ErrPeer) Error() string { return fmt.Sprintf("Error on peer %v: %v", e.Peer, e.Err) }
type Peer interface { type Peer struct {
url string
newConn func() (PeerConn, error)
sleepOnErr time.Duration // TODO: would be better as backoff
id string
connCurrent PeerConn
connBad bool // If true, don't try anything
connLock sync.Mutex
}
func newPeer(url string, newConn func() (PeerConn, error), sleepOnErr time.Duration) (*Peer, error) {
p := &Peer{url: url, newConn: newConn, sleepOnErr: sleepOnErr}
var err error
if p.connCurrent, err = newConn(); err != nil {
return nil, err
}
return p, nil
}
func (p *Peer) ID() string { return p.id }
func (p *Peer) String() string {
id := ""
if p.id != "" {
id = "(id: " + p.id + ")"
}
connStatus := "connected"
if p.Conn() == nil {
connStatus = "disconnected"
}
return fmt.Sprintf("Peer%v %v (%v)", id, p.url, connStatus)
}
func (p *Peer) reconnect() (err error) {
p.connLock.Lock()
defer p.connLock.Unlock()
if p.connCurrent == nil && p.connBad {
p.connBad = false
if p.connCurrent, err = p.newConn(); err != nil {
p.connBad = true
time.AfterFunc(p.sleepOnErr, func() { p.reconnect() })
}
}
return
}
// Can be nil peer if currently bad or closed
func (p *Peer) Conn() PeerConn {
p.connLock.Lock()
defer p.connLock.Unlock()
return p.connCurrent
}
func (p *Peer) markConnErrored(conn PeerConn) {
p.connLock.Lock()
defer p.connLock.Unlock()
if conn == p.connCurrent {
p.connCurrent = nil
p.connBad = true
conn.Close()
time.AfterFunc(p.sleepOnErr, func() { p.reconnect() })
}
}
func (p *Peer) send(ctx context.Context, msg *Message, moreMsgs ...*Message) (ok bool, err error) {
conn := p.Conn()
if conn == nil {
return false, nil
}
// Clone them with peer "to"
updatedMsg := &Message{}
*updatedMsg = *msg
updatedMsg.To = p.url
updatedMoreMsgs := make([]*Message, len(moreMsgs))
for i, moreMsg := range moreMsgs {
updatedMoreMsg := &Message{}
*updatedMoreMsg = *moreMsg
updatedMoreMsg.To = p.url
updatedMoreMsgs[i] = updatedMoreMsg
}
if err = conn.Send(ctx, updatedMsg, updatedMoreMsgs...); err != nil {
p.markConnErrored(conn)
return false, err
} else {
return true, nil
}
}
func (p *Peer) receive(ctx context.Context) (ok bool, msgs []*Message, err error) {
if conn := p.Conn(); conn == nil {
return false, nil, nil
} else if msgs, err = conn.Receive(ctx); err != nil {
p.markConnErrored(conn)
return false, nil, err
} else {
return true, msgs, nil
}
}
func (p *Peer) Close() error {
p.connLock.Lock()
defer p.connLock.Unlock()
var err error
if p.connCurrent != nil {
err = p.connCurrent.Close()
p.connCurrent = nil
}
p.connBad = false
return err
}
func (p *Peer) Closed() bool {
p.connLock.Lock()
defer p.connLock.Unlock()
return p.connCurrent == nil && !p.connBad
}
type PeerConn interface {
Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error
// Chan is closed on first err, when context is closed, or when peer is closed // Chan is closed on first err, when context is closed, or when peer is closed
Receive(ctx context.Context) ([]*Message, error) Receive(ctx context.Context) ([]*Message, error)
Close() error Close() error
} }
var PeerURLSchemes map[string]func(context.Context, *url.URL) (Peer, error) var PeerURLSchemes map[string]func(context.Context, *url.URL) (PeerConn, error)
func init() { func init() {
PeerURLSchemes = map[string]func(context.Context, *url.URL) (Peer, error){ PeerURLSchemes = map[string]func(context.Context, *url.URL) (PeerConn, error){
"http": func(ctx context.Context, peerURL *url.URL) (Peer, error) { "http": func(ctx context.Context, peerURL *url.URL) (PeerConn, error) {
schemeChangedURL := &url.URL{} schemeChangedURL := &url.URL{}
*schemeChangedURL = *peerURL *schemeChangedURL = *peerURL
schemeChangedURL.Scheme = "ws" schemeChangedURL.Scheme = "ws"
return NewPeerWebSocket(ctx, schemeChangedURL) return NewPeerConnWebSocket(ctx, schemeChangedURL)
}, },
"ws": func(ctx context.Context, peerURL *url.URL) (Peer, error) { "ws": func(ctx context.Context, peerURL *url.URL) (PeerConn, error) {
return NewPeerWebSocket(ctx, peerURL) return NewPeerConnWebSocket(ctx, peerURL)
}, },
} }
} }
func NewPeer(ctx context.Context, peerURL string) (Peer, error) { func NewPeerConn(ctx context.Context, peerURL string) (PeerConn, error) {
if parsedURL, err := url.Parse(peerURL); err != nil { if parsedURL, err := url.Parse(peerURL); err != nil {
return nil, err return nil, err
} else if peerNew := PeerURLSchemes[parsedURL.Scheme]; peerNew == nil { } else if peerNew := PeerURLSchemes[parsedURL.Scheme]; peerNew == nil {
@ -50,20 +169,20 @@ func NewPeer(ctx context.Context, peerURL string) (Peer, error) {
} }
} }
type PeerWebSocket struct { type PeerConnWebSocket struct {
Underlying *websocket.Conn Underlying *websocket.Conn
WriteLock sync.Mutex WriteLock sync.Mutex
} }
func NewPeerWebSocket(ctx context.Context, peerUrl *url.URL) (*PeerWebSocket, error) { func NewPeerConnWebSocket(ctx context.Context, peerUrl *url.URL) (*PeerConnWebSocket, error) {
conn, _, err := websocket.DefaultDialer.DialContext(ctx, peerUrl.String(), nil) conn, _, err := websocket.DefaultDialer.DialContext(ctx, peerUrl.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &PeerWebSocket{Underlying: conn}, nil return &PeerConnWebSocket{Underlying: conn}, nil
} }
func (p *PeerWebSocket) Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error { func (p *PeerConnWebSocket) Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error {
// If there are more, send all as an array of JSON strings, otherwise just the msg // If there are more, send all as an array of JSON strings, otherwise just the msg
var toWrite interface{} var toWrite interface{}
if len(moreMsgs) == 0 { if len(moreMsgs) == 0 {
@ -97,7 +216,7 @@ func (p *PeerWebSocket) Send(ctx context.Context, msg *Message, moreMsgs ...*Mes
} }
} }
func (p *PeerWebSocket) Receive(ctx context.Context) ([]*Message, error) { func (p *PeerConnWebSocket) Receive(ctx context.Context) ([]*Message, error) {
bytsCh := make(chan []byte, 1) bytsCh := make(chan []byte, 1)
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
@ -135,6 +254,6 @@ func (p *PeerWebSocket) Receive(ctx context.Context) ([]*Message, error) {
} }
} }
func (p *PeerWebSocket) Close() error { func (p *PeerConnWebSocket) Close() error {
return p.Underlying.Close() return p.Underlying.Close()
} }

View File

@ -22,11 +22,10 @@ func (s *Scoped) FetchOneLocal(ctx context.Context) *FetchResult {
// Need parent soul for lookup // Need parent soul for lookup
var parentSoul string var parentSoul string
if parentSoul, r.Err = s.parent.Soul(ctx); r.Err == nil { if parentSoul, r.Err = s.parent.Soul(ctx); r.Err == nil {
var vs *ValueWithState if r.Value, r.State, r.Err = s.gun.storage.Get(ctx, parentSoul, s.field); r.Err == ErrStorageNotFound {
if vs, r.Err = s.gun.storage.Get(ctx, parentSoul, s.field); r.Err == ErrStorageNotFound {
r.Err = nil r.Err = nil
} else if r.Err == nil { } else if r.Err == nil {
r.Value, r.State, r.ValueExists = vs.Value, vs.State, true r.ValueExists = true
} }
} }
return r return r
@ -89,7 +88,7 @@ func (s *Scoped) fetchRemote(ctx context.Context, ch chan *FetchResult) {
s.fetchResultListeners[ch] = &fetchResultListener{req.ID, ch, msgCh} s.fetchResultListeners[ch] = &fetchResultListener{req.ID, ch, msgCh}
s.fetchResultListenersLock.Unlock() s.fetchResultListenersLock.Unlock()
// Listen for responses to this get // Listen for responses to this get
s.gun.RegisterMessageIDListener(req.ID, msgCh) s.gun.registerMessageIDListener(req.ID, msgCh)
// TODO: only for children: s.gun.RegisterValueIDListener(s.id, msgCh) // TODO: only for children: s.gun.RegisterValueIDListener(s.id, msgCh)
// Handle received messages turning them to value fetches // Handle received messages turning them to value fetches
go func() { go func() {
@ -103,7 +102,7 @@ func (s *Scoped) fetchRemote(ctx context.Context, ch chan *FetchResult) {
if !ok { if !ok {
return return
} }
r := &FetchResult{Field: s.field, peer: msg.peer} r := &FetchResult{Field: s.field, Peer: msg.Peer}
// We asked for a single field, should only get that field or it doesn't exist // We asked for a single field, should only get that field or it doesn't exist
if msg.Err != "" { if msg.Err != "" {
r.Err = fmt.Errorf("Remote error: %v", msg.Err) r.Err = fmt.Errorf("Remote error: %v", msg.Err)
@ -119,11 +118,11 @@ func (s *Scoped) fetchRemote(ctx context.Context, ch chan *FetchResult) {
}() }()
// Send async, sending back errors // Send async, sending back errors
go func() { go func() {
for peerErr := range s.gun.Send(ctx, req) { for peerErr := range s.gun.send(ctx, req, nil) {
safeFetchResultSend(ch, &FetchResult{ safeFetchResultSend(ch, &FetchResult{
Err: peerErr.Err, Err: peerErr.Err,
Field: s.field, Field: s.field,
peer: peerErr.peer, Peer: peerErr.Peer,
}) })
} }
}() }()
@ -136,7 +135,7 @@ func (s *Scoped) FetchDone(ch <-chan *FetchResult) bool {
s.fetchResultListenersLock.Unlock() s.fetchResultListenersLock.Unlock()
if l != nil { if l != nil {
// Unregister the chan // Unregister the chan
s.gun.UnregisterMessageIDListener(l.id) s.gun.unregisterMessageIDListener(l.id)
// Close the message chan and the result chan // Close the message chan and the result chan
close(l.receivedMessages) close(l.receivedMessages)
close(l.results) close(l.results)
@ -162,8 +161,8 @@ type FetchResult struct {
Field string Field string
// Nil if the value doesn't exist, exists and is nil, or there's an error // Nil if the value doesn't exist, exists and is nil, or there's an error
Value Value Value Value
State int64 // This can be 0 for errors or top-level value relations State State // This can be 0 for errors or top-level value relations
ValueExists bool ValueExists bool
// Nil when local and sometimes on error // Nil when local and sometimes on error
peer *gunPeer Peer *Peer
} }

View File

@ -13,8 +13,8 @@ type putResultListener struct {
type PutResult struct { type PutResult struct {
Err error Err error
// Nil on error or local put success
peer *gunPeer Peer *Peer
} }
type PutOption interface{} type PutOption interface{}
@ -71,7 +71,7 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
} }
// We know that the first has a soul // We know that the first has a soul
prevParentSoul := parents[0].cachedSoul() prevParentSoul := parents[0].cachedSoul()
currState := TimeNowUnixMs() currState := StateNow()
for _, parent := range parents[1:] { for _, parent := range parents[1:] {
parentCachedSoul := parent.cachedSoul() parentCachedSoul := parent.cachedSoul()
if parentCachedSoul == "" { if parentCachedSoul == "" {
@ -80,15 +80,14 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
req.Put[prevParentSoul] = &Node{ req.Put[prevParentSoul] = &Node{
Metadata: Metadata{ Metadata: Metadata{
Soul: prevParentSoul, Soul: prevParentSoul,
State: map[string]int64{parent.field: currState}, State: map[string]State{parent.field: currState},
}, },
Values: map[string]Value{parent.field: ValueRelation(parentCachedSoul)}, Values: map[string]Value{parent.field: ValueRelation(parentCachedSoul)},
} }
// Also store locally and set the cached soul // Also store locally and set the cached soul
withState := &ValueWithState{ValueRelation(parentCachedSoul), currState}
// TODO: Should I not store until the very end just in case it errors halfway // TODO: Should I not store until the very end just in case it errors halfway
// though? There are no standard cases where it should fail. // though? There are no standard cases where it should fail.
if ok, err := s.gun.storage.Put(ctx, prevParentSoul, parent.field, withState); err != nil { if ok, err := s.gun.storage.Put(ctx, prevParentSoul, parent.field, ValueRelation(parentCachedSoul), currState); err != nil {
ch <- &PutResult{Err: err} ch <- &PutResult{Err: err}
close(ch) close(ch)
return ch return ch
@ -104,9 +103,8 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
} }
prevParentSoul = parentCachedSoul prevParentSoul = parentCachedSoul
} }
// Now that we've setup all the parents, we can do this store locally. // Now that we've setup all the parents, we can do this store locally
withState := &ValueWithState{val, currState} if ok, err := s.gun.storage.Put(ctx, prevParentSoul, s.field, val, currState); err != nil {
if ok, err := s.gun.storage.Put(ctx, prevParentSoul, s.field, withState); err != nil {
ch <- &PutResult{Err: err} ch <- &PutResult{Err: err}
close(ch) close(ch)
return ch return ch
@ -125,7 +123,7 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
req.Put[prevParentSoul] = &Node{ req.Put[prevParentSoul] = &Node{
Metadata: Metadata{ Metadata: Metadata{
Soul: prevParentSoul, Soul: prevParentSoul,
State: map[string]int64{s.field: currState}, State: map[string]State{s.field: currState},
}, },
Values: map[string]Value{s.field: val}, Values: map[string]Value{s.field: val},
} }
@ -134,7 +132,7 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
s.putResultListenersLock.Lock() s.putResultListenersLock.Lock()
s.putResultListeners[ch] = &putResultListener{req.ID, ch, msgCh} s.putResultListeners[ch] = &putResultListener{req.ID, ch, msgCh}
s.putResultListenersLock.Unlock() s.putResultListenersLock.Unlock()
s.gun.RegisterMessageIDListener(req.ID, msgCh) s.gun.registerMessageIDListener(req.ID, msgCh)
// Start message listener // Start message listener
go func() { go func() {
for { for {
@ -147,7 +145,7 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
if !ok { if !ok {
return return
} }
r := &PutResult{peer: msg.peer} r := &PutResult{Peer: msg.Peer}
if msg.Err != "" { if msg.Err != "" {
r.Err = fmt.Errorf("Remote error: %v", msg.Err) r.Err = fmt.Errorf("Remote error: %v", msg.Err)
} else if msg.OK != 1 { } else if msg.OK != 1 {
@ -159,10 +157,10 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
}() }()
// Send async, sending back errors // Send async, sending back errors
go func() { go func() {
for peerErr := range s.gun.Send(ctx, req) { for peerErr := range s.gun.send(ctx, req, nil) {
safePutResultSend(ch, &PutResult{ safePutResultSend(ch, &PutResult{
Err: peerErr.Err, Err: peerErr.Err,
peer: peerErr.peer, Peer: peerErr.Peer,
}) })
} }
}() }()
@ -176,7 +174,7 @@ func (s *Scoped) PutDone(ch <-chan *PutResult) bool {
s.putResultListenersLock.Unlock() s.putResultListenersLock.Unlock()
if l != nil { if l != nil {
// Unregister the chan // Unregister the chan
s.gun.UnregisterMessageIDListener(l.id) s.gun.unregisterMessageIDListener(l.id)
// Close the message chan and the result chan // Close the message chan and the result chan
close(l.receivedMessages) close(l.receivedMessages)
close(l.results) close(l.results)

View File

@ -5,30 +5,34 @@ import (
"time" "time"
) )
// TimeFromUnixMs returns zero'd time if ms is 0 type State uint64
func TimeFromUnixMs(ms int64) time.Time {
func StateNow() State { return State(timeNowUnixMs()) }
// timeFromUnixMs returns zero'd time if ms is 0
func timeFromUnixMs(ms int64) time.Time {
if ms == 0 { if ms == 0 {
return time.Time{} return time.Time{}
} }
return time.Unix(0, ms*int64(time.Millisecond)) return time.Unix(0, ms*int64(time.Millisecond))
} }
// TimeToUnixMs returns 0 if t.IsZero // timeToUnixMs returns 0 if t.IsZero
func TimeToUnixMs(t time.Time) int64 { func timeToUnixMs(t time.Time) int64 {
if t.IsZero() { if t.IsZero() {
return 0 return 0
} }
return t.UnixNano() / int64(time.Millisecond) return t.UnixNano() / int64(time.Millisecond)
} }
func TimeNowUnixMs() int64 { func timeNowUnixMs() int64 {
return TimeToUnixMs(time.Now()) return timeToUnixMs(time.Now())
} }
var lastNano int64 var lastNano int64
// uniqueNano is 0 if ms is first time seen, otherwise a unique num in combination with ms // uniqueNano is 0 if ms is first time seen, otherwise a unique num in combination with ms
func TimeNowUniqueUnix() (ms int64, uniqueNum int64) { func timeNowUniqueUnix() (ms int64, uniqueNum int64) {
now := time.Now() now := time.Now()
newNano := now.UnixNano() newNano := now.UnixNano()
for { for {

View File

@ -9,9 +9,9 @@ import (
var ErrStorageNotFound = errors.New("Not found") var ErrStorageNotFound = errors.New("Not found")
type Storage interface { type Storage interface {
Get(ctx context.Context, parentSoul, field string) (*ValueWithState, error) Get(ctx context.Context, parentSoul, field string) (Value, State, error)
// If bool is false, it's deferred // If bool is false, it's deferred
Put(ctx context.Context, parentSoul, field string, val *ValueWithState) (bool, error) Put(ctx context.Context, parentSoul, field string, val Value, state State) (bool, error)
Tracking(ctx context.Context, parentSoul, field string) (bool, error) Tracking(ctx context.Context, parentSoul, field string) (bool, error)
} }
@ -21,16 +21,22 @@ type StorageInMem struct {
type parentSoulAndField struct{ parentSoul, field string } type parentSoulAndField struct{ parentSoul, field string }
func (s *StorageInMem) Get(ctx context.Context, parentSoul, field string) (*ValueWithState, error) { type valueWithState struct {
v, ok := s.values.Load(parentSoulAndField{parentSoul, field}) val Value
if !ok { state State
return nil, ErrStorageNotFound
}
return v.(*ValueWithState), nil
} }
func (s *StorageInMem) Put(ctx context.Context, parentSoul, field string, val *ValueWithState) (bool, error) { func (s *StorageInMem) Get(ctx context.Context, parentSoul, field string) (Value, State, error) {
s.values.Store(parentSoulAndField{parentSoul, field}, val) v, ok := s.values.Load(parentSoulAndField{parentSoul, field})
if !ok {
return nil, 0, ErrStorageNotFound
}
vs := v.(*valueWithState)
return vs.val, vs.state, nil
}
func (s *StorageInMem) Put(ctx context.Context, parentSoul, field string, val Value, state State) (bool, error) {
s.values.Store(parentSoulAndField{parentSoul, field}, &valueWithState{val, state})
// TODO: conflict resolution state check? // TODO: conflict resolution state check?
return true, nil return true, nil
} }

View File

@ -26,6 +26,15 @@ func newContext(t *testing.T) (*testContext, context.CancelFunc) {
return withTestContext(context.Background(), t) return withTestContext(context.Background(), t)
} }
func newContextWithGunJServer(t *testing.T) (*testContext, context.CancelFunc) {
ctx, cancelFn := newContext(t)
serverCancelFn := ctx.startGunJSServer()
return ctx, func() {
serverCancelFn()
cancelFn()
}
}
const defaultGunJSPort = 8080 const defaultGunJSPort = 8080
func withTestContext(ctx context.Context, t *testing.T) (*testContext, context.CancelFunc) { func withTestContext(ctx context.Context, t *testing.T) (*testContext, context.CancelFunc) {
@ -77,20 +86,26 @@ func (t *testContext) startJS(script string) (*bytes.Buffer, *exec.Cmd, context.
return &buf, cmd, cancelFn return &buf, cmd, cancelFn
} }
func (t *testContext) startGunJSServer() { func (t *testContext) startGunJSServer() context.CancelFunc {
// If we're logging, use a proxy // If we're logging, use a proxy
port := t.GunJSPort port := t.GunJSPort
if testing.Verbose() { if testing.Verbose() {
t.startGunWebSocketProxyLogger(port, port+1) t.startGunWebSocketProxyLogger(port, port+1)
port++ port++
} }
// Remove entire data folder first // Remove entire data folder first just in case
t.Require.NoError(os.RemoveAll("radata-server")) t.Require.NoError(os.RemoveAll("radata-server"))
t.startJS(` _, cmd, cancelFn := t.startJS(`
var Gun = require('gun') var Gun = require('gun')
const server = require('http').createServer().listen(` + strconv.Itoa(port) + `) const server = require('http').createServer().listen(` + strconv.Itoa(port) + `)
const gun = Gun({web: server, file: 'radata-server'}) const gun = Gun({web: server, file: 'radata-server'})
`) `)
return func() {
cancelFn()
cmd.Wait()
// Remove the data folder at the end
os.RemoveAll("radata-server")
}
} }
func (t *testContext) newGunConnectedToGunJS() *gun.Gun { func (t *testContext) newGunConnectedToGunJS() *gun.Gun {

View File

@ -9,9 +9,8 @@ import (
func TestGunGetSimple(t *testing.T) { func TestGunGetSimple(t *testing.T) {
// Run the server, put in one call, get in another, then check // Run the server, put in one call, get in another, then check
ctx, cancelFn := newContext(t) ctx, cancelFn := newContextWithGunJServer(t)
defer cancelFn() defer cancelFn()
ctx.startGunJSServer()
randStr := randString(30) randStr := randString(30)
// Write w/ JS // Write w/ JS
ctx.runJSWithGun(` ctx.runJSWithGun(`
@ -38,9 +37,8 @@ func TestGunGetSimple(t *testing.T) {
} }
func TestGunPutSimple(t *testing.T) { func TestGunPutSimple(t *testing.T) {
ctx, cancelFn := newContext(t) ctx, cancelFn := newContextWithGunJServer(t)
defer cancelFn() defer cancelFn()
ctx.startGunJSServer()
randStr := randString(30) randStr := randString(30)
// Put // Put
g := ctx.newGunConnectedToGunJS() g := ctx.newGunConnectedToGunJS()

View File

@ -13,9 +13,8 @@ func TestSimpleJS(t *testing.T) {
func TestGunJS(t *testing.T) { func TestGunJS(t *testing.T) {
// Run the server, put in one call, get in another, then check // Run the server, put in one call, get in another, then check
ctx, cancelFn := newContext(t) ctx, cancelFn := newContextWithGunJServer(t)
defer cancelFn() defer cancelFn()
ctx.startGunJSServer()
randStr := randString(30) randStr := randString(30)
ctx.runJSWithGun(` ctx.runJSWithGun(`
gun.get('esgopeta-test').get('TestGunJS').get('some-key').put('` + randStr + `', ack => { gun.get('esgopeta-test').get('TestGunJS').get('some-key').put('` + randStr + `', ack => {