announce still needs tests, but i tested a lot by hand and its good

This commit is contained in:
Alex Grintsvayg 2018-07-26 21:30:22 -04:00
parent 5378fcbb94
commit 38eaa17a9b
3 changed files with 85 additions and 56 deletions

View file

@ -12,9 +12,8 @@ const (
Network = "udp4" Network = "udp4"
DefaultPort = 4444 DefaultPort = 4444
DefaultAnnounceRate = 10 DefaultAnnounceRate = 10 // send at most this many announces per second
DefaultAnnounceBurst = 1 DefaultReannounceTime = 50 * time.Minute // should be a bit less than hash expiration time
DefaultReannounceTime = 50 * time.Minute
// TODO: all these constants should be defaults, and should be used to set values in the standard Config. then the code should use values in the config // TODO: all these constants should be defaults, and should be used to set values in the standard Config. then the code should use values in the config
// TODO: alternatively, have a global Config for constants. at least that way tests can modify the values // TODO: alternatively, have a global Config for constants. at least that way tests can modify the values
@ -55,8 +54,8 @@ type Config struct {
ReannounceTime time.Duration ReannounceTime time.Duration
// send at most this many announces per second // send at most this many announces per second
AnnounceRate int AnnounceRate int
// allow bursts of up to this many times the announce rate // channel that will receive notifications about announcements
AnnounceBurst int AnnounceNotificationCh chan announceNotification
} }
// NewStandardConfig returns a Config pointer with default values. // NewStandardConfig returns a Config pointer with default values.
@ -71,6 +70,5 @@ func NewStandardConfig() *Config {
PeerProtocolPort: peerproto.DefaultPort, PeerProtocolPort: peerproto.DefaultPort,
ReannounceTime: DefaultReannounceTime, ReannounceTime: DefaultReannounceTime,
AnnounceRate: DefaultAnnounceRate, AnnounceRate: DefaultAnnounceRate,
AnnounceBurst: DefaultAnnounceBurst,
} }
} }

View file

@ -3,6 +3,7 @@ package dht
import ( import (
"container/ring" "container/ring"
"context" "context"
"math"
"sync" "sync"
"time" "time"
@ -16,6 +17,17 @@ type queueEdit struct {
add bool add bool
} }
const (
announceStarted = "started"
announceFinishd = "finished"
)
type announceNotification struct {
hash bits.Bitmap
action string
err error
}
// Add adds the hash to the list of hashes this node is announcing // Add adds the hash to the list of hashes this node is announcing
func (dht *DHT) Add(hash bits.Bitmap) { func (dht *DHT) Add(hash bits.Bitmap) {
dht.announceAddRemove <- queueEdit{hash: hash, add: true} dht.announceAddRemove <- queueEdit{hash: hash, add: true}
@ -32,34 +44,69 @@ func (dht *DHT) runAnnouncer() {
lastAnnounce time.Time lastAnnounce time.Time
} }
queue := ring.New(0) var queue *ring.Ring
hashes := make(map[bits.Bitmap]*ring.Ring) hashes := make(map[bits.Bitmap]*ring.Ring)
limiter := rate.NewLimiter(rate.Limit(dht.conf.AnnounceRate), dht.conf.AnnounceRate*dht.conf.AnnounceBurst)
var announceNextHash <-chan time.Time var announceNextHash <-chan time.Time
timer := time.NewTimer(0) timer := time.NewTimer(math.MaxInt64)
closedCh := make(chan time.Time) timer.Stop()
close(closedCh)
limitCh := make(chan time.Time)
dht.grp.Add(1)
go func() {
defer dht.grp.Done()
limiter := rate.NewLimiter(rate.Limit(dht.conf.AnnounceRate), dht.conf.AnnounceRate)
for {
limiter.Wait(context.Background()) // TODO: should use grp.ctx somehow? so when grp is closed, wait returns
select {
case limitCh <- time.Now():
case <-dht.grp.Ch():
return
}
}
}()
maintenance := time.NewTicker(1 * time.Minute)
// TODO: work to space hash announces out so they aren't bunched up around the reannounce time. track time since last announce. if its been more than the ideal time (reannounce time / numhashes), start announcing hashes early
for { for {
select { select {
case <-dht.grp.Ch(): case <-dht.grp.Ch():
return
case <-maintenance.C:
maxAnnounce := dht.conf.AnnounceRate * int(dht.conf.ReannounceTime.Seconds())
if len(hashes) > maxAnnounce {
// TODO: send this to slack
log.Warnf("DHT has %d hashes, but can only announce %d hashes in the %s reannounce window. Raise the announce rate or spawn more nodes.",
len(hashes), maxAnnounce, dht.conf.ReannounceTime.String())
}
case change := <-dht.announceAddRemove: case change := <-dht.announceAddRemove:
if change.add { if change.add {
if _, exists := hashes[change.hash]; exists {
continue
}
r := ring.New(1) r := ring.New(1)
r.Value = hashAndTime{hash: change.hash} r.Value = hashAndTime{hash: change.hash}
if queue != nil {
queue.Prev().Link(r) queue.Prev().Link(r)
}
queue = r queue = r
hashes[change.hash] = r hashes[change.hash] = r
announceNextHash = closedCh // don't wait to announce next hash announceNextHash = limitCh // announce next hash ASAP
} else { } else {
if r, exists := hashes[change.hash]; exists { r, exists := hashes[change.hash]
if !exists {
continue
}
delete(hashes, change.hash) delete(hashes, change.hash)
if len(hashes) == 0 { if len(hashes) == 0 {
queue = ring.New(0) queue = ring.New(0)
announceNextHash = make(chan time.Time) // no hashes to announce, wait indefinitely announceNextHash = nil // no hashes to announce, wait indefinitely
} else { } else {
if r == queue { if r == queue {
queue = queue.Next() // don't lose our pointer queue = queue.Next() // don't lose our pointer
@ -67,33 +114,46 @@ func (dht *DHT) runAnnouncer() {
r.Prev().Link(r.Next()) r.Prev().Link(r.Next())
} }
} }
}
case <-announceNextHash: case <-announceNextHash:
limiter.Wait(context.Background()) // TODO: should use grp.ctx somehow
dht.grp.Add(1) dht.grp.Add(1)
ht := queue.Value.(hashAndTime) ht := queue.Value.(hashAndTime)
if !ht.lastAnnounce.IsZero() { if !ht.lastAnnounce.IsZero() {
nextAnnounce := ht.lastAnnounce.Add(dht.conf.ReannounceTime) nextAnnounce := ht.lastAnnounce.Add(dht.conf.ReannounceTime)
if nextAnnounce.Before(time.Now()) { if nextAnnounce.After(time.Now()) {
timer.Reset(time.Until(nextAnnounce)) timer.Reset(time.Until(nextAnnounce))
announceNextHash = timer.C // wait until next hash should be announced announceNextHash = timer.C // wait until next hash should be announced
continue continue
} }
} }
if dht.conf.AnnounceNotificationCh != nil {
dht.conf.AnnounceNotificationCh <- announceNotification{
hash: ht.hash,
action: announceStarted,
}
}
go func(hash bits.Bitmap) { go func(hash bits.Bitmap) {
defer dht.grp.Done() defer dht.grp.Done()
err := dht.announce(hash) err := dht.announce(hash)
if err != nil { if err != nil {
log.Error(errors.Prefix("announce", err)) log.Error(errors.Prefix("announce", err))
} }
if dht.conf.AnnounceNotificationCh != nil {
dht.conf.AnnounceNotificationCh <- announceNotification{
hash: ht.hash,
action: announceFinishd,
err: err,
}
}
}(ht.hash) }(ht.hash)
queue.Value = hashAndTime{hash: ht.hash, lastAnnounce: time.Now()} queue.Value = hashAndTime{hash: ht.hash, lastAnnounce: time.Now()}
queue = queue.Next() queue = queue.Next()
announceNextHash = closedCh // don't wait to announce next hash announceNextHash = limitCh // announce next hash ASAP
} }
} }
} }

View file

@ -1,29 +0,0 @@
package dht
import (
"testing"
)
func TestDHT_Announce(t *testing.T) {
t.Skip("NEED SOME TESTS FOR ANNOUNCING")
// tests
// - max rate
// - new announces get ahead of old announces
// - announcer blocks correctly (when nothing to announce, when next announce time is in the future) and unblocks correctly (when waiting to announce next and a new hash is added)
// thought: what happens when you're waiting to announce a hash and it gets removed? probably nothing, since later hashes will be announced later. but still good to test this
//
//bs, dhts := TestingCreateNetwork(t, 2, true, true)
//defer func() {
// for _, d := range dhts {
// go d.Shutdown()
// }
// bs.Shutdown()
// time.Sleep(1 * time.Second)
//}()
//
//announcer := dhts[0]
//receiver := dhts[1]
}