Files
weave/connection_maker.go
T
Matthias Radestock eecaa567fc eliminate periodic 'tick' in connection_maker
checkStateAndAttemptConnections already performs a full scan of all
potential targets, from which we can easily determine the nearest
future time (or, rather, its duration from now) on which an attempt is
due.

We also need to kick the ConnectionMaker into action when we receive a
topology update containing new connections.
2014-09-22 23:15:54 +01:00

200 lines
5.1 KiB
Go

package weave
import (
"bytes"
"fmt"
"log"
"math"
"math/rand"
"net"
"time"
)
const (
InitialInterval = 5 * time.Second
MaxInterval = 10 * time.Minute
)
const (
CMInitiate = iota
CMTerminated = iota
CMRefresh = iota
CMStatus = iota
)
func StartConnectionMaker(router *Router) *ConnectionMaker {
queryChan := make(chan *ConnectionMakerInteraction, ChannelSize)
state := &ConnectionMaker{
router: router,
queryChan: queryChan,
cmdLineAddress: make(map[string]bool),
targets: make(map[string]*Target)}
go state.queryLoop(queryChan)
return state
}
func (cm *ConnectionMaker) InitiateConnection(address string) {
cm.queryChan <- &ConnectionMakerInteraction{
Interaction: Interaction{code: CMInitiate},
address: address}
}
func (cm *ConnectionMaker) ConnectionTerminated(address string) {
cm.queryChan <- &ConnectionMakerInteraction{
Interaction: Interaction{code: CMTerminated},
address: address}
}
func (cm *ConnectionMaker) Refresh() {
cm.queryChan <- &ConnectionMakerInteraction{
Interaction: Interaction{code: CMRefresh}}
}
func (cm *ConnectionMaker) String() string {
resultChan := make(chan interface{}, 0)
cm.queryChan <- &ConnectionMakerInteraction{
Interaction: Interaction{code: CMStatus, resultChan: resultChan}}
result := <-resultChan
return result.(string)
}
func (cm *ConnectionMaker) queryLoop(queryChan <-chan *ConnectionMakerInteraction) {
timer := time.NewTimer(math.MaxInt64)
run := func() { timer.Reset(cm.checkStateAndAttemptConnections()) }
for {
select {
case query, ok := <-queryChan:
if !ok {
return
}
switch query.code {
case CMInitiate:
cm.cmdLineAddress[NormalisePeerAddr(query.address)] = true
run()
case CMTerminated:
if target, found := cm.targets[query.address]; found {
target.attempting = false
target.tryAfter, target.tryInterval = tryAfter(target.tryInterval)
}
run()
case CMRefresh:
run()
case CMStatus:
query.resultChan <- cm.status()
default:
log.Fatal("Unexpected connection maker query:", query)
}
case <-timer.C:
run()
}
}
}
func (cm *ConnectionMaker) checkStateAndAttemptConnections() time.Duration {
ourself := cm.router.Ourself
validTarget := make(map[string]bool)
// copy the set of things we are connected to, so we can access them without locking
our_connected_peers := make(map[PeerName]bool)
our_connected_targets := make(map[string]bool)
ourself.ForEachConnection(func(peer PeerName, conn Connection) {
our_connected_peers[peer] = true
our_connected_targets[conn.RemoteTCPAddr()] = true
})
addTarget := func(address string) {
if !our_connected_targets[address] {
validTarget[address] = true
cm.addTarget(address)
}
}
// Add command-line targets that are not connected
for address, _ := range cm.cmdLineAddress {
addTarget(address)
}
// Add targets for peers that someone else is connected to, but we
// aren't
cm.router.Peers.ForEach(func(name PeerName, peer *Peer) {
peer.ForEachConnection(func(otherPeer PeerName, conn Connection) {
if otherPeer == ourself.Name || our_connected_peers[otherPeer] {
return
}
address := conn.RemoteTCPAddr()
// try both portnumber of connection and standard port
addTarget(address)
if host, _, err := net.SplitHostPort(address); err == nil {
addTarget(NormalisePeerAddr(host))
}
})
})
now := time.Now() // make sure we catch items just added
after := time.Duration(math.MaxInt64)
for address, target := range cm.targets {
if our_connected_targets[address] {
delete(cm.targets, address)
continue
}
if target.attempting {
continue
}
if !validTarget[address] {
delete(cm.targets, address)
continue
}
switch duration := target.tryAfter.Sub(now); {
case duration <= 0:
target.attempting = true
go cm.attemptConnection(address, cm.cmdLineAddress[address])
case duration < after:
after = duration
}
}
return after
}
func (cm *ConnectionMaker) addTarget(address string) {
if _, found := cm.targets[address]; !found {
target := &Target{}
target.tryAfter, target.tryInterval = tryImmediately()
cm.targets[address] = target
}
}
func (cm *ConnectionMaker) status() string {
var buf bytes.Buffer
for address, target := range cm.targets {
var fmtStr string
if target.attempting {
fmtStr = "%s (trying since %v)\n"
} else {
fmtStr = "%s (next try at %v)\n"
}
buf.WriteString(fmt.Sprintf(fmtStr, address, target.tryAfter))
}
return buf.String()
}
func (cm *ConnectionMaker) attemptConnection(address string, acceptNewPeer bool) {
log.Println("Attempting connection to", address)
if err := cm.router.Ourself.CreateConnection(address, acceptNewPeer); err != nil {
log.Println(err)
cm.ConnectionTerminated(address)
}
}
func tryImmediately() (time.Time, time.Duration) {
interval := time.Duration(rand.Int63n(int64(InitialInterval)))
return time.Now(), interval
}
func tryAfter(interval time.Duration) (time.Time, time.Duration) {
interval += time.Duration(rand.Int63n(int64(interval)))
if interval > MaxInterval {
interval = MaxInterval
}
return time.Now().Add(interval), interval
}