diff --git a/protocol/connections/engine.go b/protocol/connections/engine.go index 334a3f6..774e210 100644 --- a/protocol/connections/engine.go +++ b/protocol/connections/engine.go @@ -160,7 +160,7 @@ func (e *engine) eventHandler() { case event.SendMessageToGroup: ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext]) signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature]) - go e.sendMessageToGroup(ev.Data[event.GroupID], ev.Data[event.GroupServer], ciphertext, signature) + go e.sendMessageToGroup(ev.Data[event.GroupID], ev.Data[event.GroupServer], ciphertext, signature, 0) case event.SendMessageToPeer: // TODO: remove this passthrough once the UI is integrated. context, ok := ev.Data[event.EventContext] @@ -480,7 +480,17 @@ func (e *engine) receiveGroupMessage(server string, gm *groups.EncryptedGroupMes } // sendMessageToGroup attempts to sent the given message to the given group id. -func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, sig []byte) { +func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, sig []byte, attempts int) { + + // sending to groups can fail for a few reasons (slow server, not enough tokens, etc.) + // rather than trying to keep all that logic in method we simply back-off and try again + // but if we fail more than 5 times then we report back to the client so they can investigate other options. + // Note: This flow only applies to online-and-connected servers (this method will return faster if the server is not + // online) + if attempts >= 5 { + e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "could not make payment to server", event.Signature: base64.StdEncoding.EncodeToString(sig)})) + return + } es, ok := e.ephemeralServices.Load(server) if es == nil || !ok { @@ -493,17 +503,16 @@ func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, si if err == nil { tokenApp, ok := (conn.App()).(*TokenBoardClient) if ok { - if spent, _ := tokenApp.Post(ct, sig); !spent { - // TODO: while this works for the spam guard, it won't work for other forms of payment... - // Make an -inline- payment, this will hold the goroutine - if err := tokenApp.MakePayment(); err == nil { - // This really shouldn't fail since we now know we have the required tokens... - if spent, numTokens := tokenApp.Post(ct, sig); !spent { - e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: fmt.Errorf("could not post %v", numTokens).Error(), event.Signature: base64.StdEncoding.EncodeToString(sig)})) - } - } else { - // Broadcast the token error - e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: err.Error(), event.Signature: base64.StdEncoding.EncodeToString(sig)})) + if spent, numtokens := tokenApp.Post(ct, sig); !spent { + // we failed to post, probably because we ran out of tokens... so make a payment + go tokenApp.MakePayment() + // backoff + time.Sleep(time.Second) + // try again + e.sendMessageToGroup(groupID, server, ct, sig, attempts+1) + } else { + if numtokens < 5 { + go tokenApp.MakePayment() } } // regardless we return.... diff --git a/protocol/connections/tokenboardclientapp.go b/protocol/connections/tokenboardclientapp.go index c57a953..dfe6810 100644 --- a/protocol/connections/tokenboardclientapp.go +++ b/protocol/connections/tokenboardclientapp.go @@ -13,6 +13,7 @@ import ( "git.openprivacy.ca/openprivacy/log" "github.com/gtank/ristretto255" "sync" + "time" ) // NewTokenBoardClient generates a new Client for Token Board @@ -190,24 +191,28 @@ func (ta *TokenBoardClient) MakePayment() error { conn, _ = client.GetConnection(ta.tokenServiceOnion) } - client.Connect(ta.tokenServiceOnion, powTokenApp) - conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability) - if err == nil { - powtapp, ok := conn.App().(*applications.TokenApplication) - if ok { - // Update tokens...we need a lock here to prevent SpendToken from modifying the tokens - // during this process.. - log.Debugf("Updating Tokens") - ta.tokenLock.Lock() - ta.tokens = append(ta.tokens, powtapp.Tokens...) - if len(ta.tokens) < 10 { - go ta.MakePayment() + connected, err := client.Connect(ta.tokenServiceOnion, powTokenApp) + if connected == true && err == nil { + conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability) + if err == nil { + powtapp, ok := conn.App().(*applications.TokenApplication) + if ok { + // Update tokens...we need a lock here to prevent SpendToken from modifying the tokens + // during this process.. + log.Debugf("Updating Tokens") + ta.tokenLock.Lock() + ta.tokens = append(ta.tokens, powtapp.Tokens...) + ta.tokenLock.Unlock() + log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit()) + conn.Close() + return nil } - ta.tokenLock.Unlock() - log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit()) - conn.Close() - return nil } + } else { + log.Debugf("failed to make a connection. trying again...") + // it doesn't actually take that long to make a payment, so waiting a small amount of time should suffice + time.Sleep(time.Second) + return ta.MakePayment() } log.Debugf("Error making payment: to %v %v", ta.tokenServiceOnion, err) return err