From 80b7406d6a5b1add97e7a4618d8a8f173e6cca64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 13 Oct 2021 17:48:17 +0200 Subject: [PATCH 1/2] autobatch: thread-safe, debounce, max delay and implement Batching Revamp the venerable autobatch in different ways: - make it thread safe (test -race is happy) - deduplicate writes (avoid writing the same key multiple time), which make it act as a debounce - introduce a maximum delay before the write happen, to avoid keeping the pending writes in memory if the count trigger is not reached - implement the Batching interface for compatibility with more usecases --- autobatch/autobatch.go | 187 +++++++++++++++++++++++++++--------- autobatch/autobatch_test.go | 31 +++++- mount/mount_test.go | 13 +-- 3 files changed, 177 insertions(+), 54 deletions(-) diff --git a/autobatch/autobatch.go b/autobatch/autobatch.go index 0f86764..4e0ceea 100644 --- a/autobatch/autobatch.go +++ b/autobatch/autobatch.go @@ -1,9 +1,13 @@ // Package autobatch provides a go-datastore implementation that // automatically batches together writes by holding puts in memory until -// a certain threshold is met. +// a certain threshold is met. It also acts as a debounce. package autobatch import ( + "log" + "sync" + "time" + ds "github.com/ipfs/go-datastore" dsq "github.com/ipfs/go-datastore/query" ) @@ -12,9 +16,13 @@ import ( type Datastore struct { child ds.Batching - // TODO: discuss making ds.Batch implement the full ds.Datastore interface - buffer map[ds.Key]op - maxBufferEntries int + mu sync.RWMutex + buffer map[ds.Key]op + + maxWrite int + maxDelay time.Duration + newWrite chan struct{} + exit chan struct{} } type op struct { @@ -23,28 +31,79 @@ type op struct { } // NewAutoBatching returns a new datastore that automatically -// batches writes using the given Batching datastore. The size -// of the memory pool is given by size. -func NewAutoBatching(d ds.Batching, size int) *Datastore { - return &Datastore{ - child: d, - buffer: make(map[ds.Key]op, size), - maxBufferEntries: size, +// batches writes using the given Batching datastore. The maximum number of +// write before triggering a batch is given by maxWrite. The maximum delay +// before triggering a batch is given by maxDelay. +func NewAutoBatching(child ds.Batching, maxWrite int, maxDelay time.Duration) *Datastore { + d := &Datastore{ + child: child, + buffer: make(map[ds.Key]op, maxWrite), + maxWrite: maxWrite, + maxDelay: maxDelay, + newWrite: make(chan struct{}), + exit: make(chan struct{}), + } + go d.runBatcher() + return d +} + +func (d *Datastore) addOp(key ds.Key, op op) { + d.mu.Lock() + d.buffer[key] = op + d.mu.Unlock() + d.newWrite <- struct{}{} +} + +func (d *Datastore) runBatcher() { + var timer <-chan time.Time + + write := func() { + timer = nil + + b, err := d.prepareBatch(nil) + if err != nil { + log.Println(err) + return + } + err = b.Commit() + if err != nil { + log.Println(err) + return + } + } + + for { + select { + case <-d.exit: + return + case <-timer: + write() + case <-d.newWrite: + d.mu.RLock() + ready := len(d.buffer) + d.mu.RUnlock() + if ready > d.maxWrite { + write() + } + if timer == nil { + timer = time.After(d.maxDelay) + } + } } } // Delete deletes a key/value func (d *Datastore) Delete(k ds.Key) error { - d.buffer[k] = op{delete: true} - if len(d.buffer) > d.maxBufferEntries { - return d.Flush() - } + d.addOp(k, op{delete: true}) return nil } // Get retrieves a value given a key. func (d *Datastore) Get(k ds.Key) ([]byte, error) { + d.mu.RLock() o, ok := d.buffer[k] + d.mu.RUnlock() + if ok { if o.delete { return nil, ds.ErrNotFound @@ -57,50 +116,42 @@ func (d *Datastore) Get(k ds.Key) ([]byte, error) { // Put stores a key/value. func (d *Datastore) Put(k ds.Key, val []byte) error { - d.buffer[k] = op{value: val} - if len(d.buffer) > d.maxBufferEntries { - return d.Flush() - } + d.addOp(k, op{value: val}) return nil } // Sync flushes all operations on keys at or under the prefix // from the current batch to the underlying datastore func (d *Datastore) Sync(prefix ds.Key) error { - b, err := d.child.Batch() + b, err := d.prepareBatch(&prefix) if err != nil { return err } - - for k, o := range d.buffer { - if !(k.Equal(prefix) || k.IsDescendantOf(prefix)) { - continue - } - - var err error - if o.delete { - err = b.Delete(k) - } else { - err = b.Put(k, o.value) - } - if err != nil { - return err - } - - delete(d.buffer, k) - } - return b.Commit() } // Flush flushes the current batch to the underlying datastore. func (d *Datastore) Flush() error { - b, err := d.child.Batch() + b, err := d.prepareBatch(nil) if err != nil { return err } + return b.Commit() +} + +func (d *Datastore) prepareBatch(prefix *ds.Key) (ds.Batch, error) { + b, err := d.child.Batch() + if err != nil { + return nil, err + } + + d.mu.Lock() for k, o := range d.buffer { + if prefix != nil && !(k.Equal(*prefix) || k.IsDescendantOf(*prefix)) { + continue + } + var err error if o.delete { err = b.Delete(k) @@ -108,18 +159,24 @@ func (d *Datastore) Flush() error { err = b.Put(k, o.value) } if err != nil { - return err + d.mu.Unlock() + return nil, err } + + delete(d.buffer, k) } - // clear out buffer - d.buffer = make(map[ds.Key]op, d.maxBufferEntries) - return b.Commit() + d.mu.Unlock() + + return b, nil } // Has checks if a key is stored. func (d *Datastore) Has(k ds.Key) (bool, error) { + d.mu.RLock() o, ok := d.buffer[k] + d.mu.RUnlock() + if ok { return !o.delete, nil } @@ -129,7 +186,10 @@ func (d *Datastore) Has(k ds.Key) (bool, error) { // GetSize implements Datastore.GetSize func (d *Datastore) GetSize(k ds.Key) (int, error) { + d.mu.RLock() o, ok := d.buffer[k] + d.mu.RUnlock() + if ok { if o.delete { return -1, ds.ErrNotFound @@ -155,6 +215,18 @@ func (d *Datastore) DiskUsage() (uint64, error) { return ds.DiskUsage(d.child) } +func (d *Datastore) Batch() (ds.Batch, error) { + b, err := d.child.Batch() + if err != nil { + return nil, err + } + return &batch{ + parent: d, + child: b, + toDelete: make(map[ds.Key]struct{}), + }, nil +} + func (d *Datastore) Close() error { err1 := d.Flush() err2 := d.child.Close() @@ -164,5 +236,32 @@ func (d *Datastore) Close() error { if err2 != nil { return err2 } + close(d.exit) + close(d.newWrite) return nil } + +type batch struct { + parent *Datastore + child ds.Batch + toDelete map[ds.Key]struct{} +} + +func (b *batch) Put(key ds.Key, value []byte) error { + delete(b.toDelete, key) + return b.child.Put(key, value) +} + +func (b *batch) Delete(key ds.Key) error { + b.toDelete[key] = struct{}{} + return b.child.Delete(key) +} + +func (b *batch) Commit() error { + b.parent.mu.Lock() + for key := range b.toDelete { + delete(b.parent.buffer, key) + } + b.parent.mu.Unlock() + return b.child.Commit() +} diff --git a/autobatch/autobatch_test.go b/autobatch/autobatch_test.go index bd6fb30..1d9fe6c 100644 --- a/autobatch/autobatch_test.go +++ b/autobatch/autobatch_test.go @@ -4,18 +4,20 @@ import ( "bytes" "fmt" "testing" + "time" ds "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/sync" dstest "github.com/ipfs/go-datastore/test" ) func TestAutobatch(t *testing.T) { - dstest.SubtestAll(t, NewAutoBatching(ds.NewMapDatastore(), 16)) + dstest.SubtestAll(t, NewAutoBatching(sync.MutexWrap(ds.NewMapDatastore()), 16, time.Second)) } func TestFlushing(t *testing.T) { - child := ds.NewMapDatastore() - d := NewAutoBatching(child, 16) + child := sync.MutexWrap(ds.NewMapDatastore()) + d := NewAutoBatching(child, 16, 500*time.Millisecond) var keys []ds.Key for i := 0; i < 16; i++ { @@ -70,6 +72,9 @@ func TestFlushing(t *testing.T) { t.Fatal(err) } + // flushing is async so we can rely on having it happening immediately + time.Sleep(100 * time.Millisecond) + // should be flushed now, try to get keys from child datastore for _, k := range keys[:14] { val, err := child.Get(k) @@ -102,11 +107,29 @@ func TestFlushing(t *testing.T) { if !bytes.Equal(val, v) { t.Fatal("wrong value") } + + // let's test the maximum delay with a single key + key17 := ds.NewKey("test17") + err = d.Put(key17, v) + if err != nil { + t.Fatal(err) + } + + time.Sleep(600 * time.Millisecond) + + val, err = child.Get(key17) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(val, v) { + t.Fatal("wrong value") + } } func TestSync(t *testing.T) { child := ds.NewMapDatastore() - d := NewAutoBatching(child, 100) + d := NewAutoBatching(child, 100, time.Second) put := func(key ds.Key) { if err := d.Put(key, []byte(key.String())); err != nil { diff --git a/mount/mount_test.go b/mount/mount_test.go index a3059b4..898d5e6 100644 --- a/mount/mount_test.go +++ b/mount/mount_test.go @@ -3,6 +3,7 @@ package mount_test import ( "errors" "testing" + "time" datastore "github.com/ipfs/go-datastore" autobatch "github.com/ipfs/go-datastore/autobatch" @@ -725,14 +726,14 @@ func TestLookupPrio(t *testing.T) { } func TestNestedMountSync(t *testing.T) { - internalDSRoot := datastore.NewMapDatastore() - internalDSFoo := datastore.NewMapDatastore() - internalDSFooBar := datastore.NewMapDatastore() + internalDSRoot := sync.MutexWrap(datastore.NewMapDatastore()) + internalDSFoo := sync.MutexWrap(datastore.NewMapDatastore()) + internalDSFooBar := sync.MutexWrap(datastore.NewMapDatastore()) m := mount.New([]mount.Mount{ - {Prefix: datastore.NewKey("/foo"), Datastore: autobatch.NewAutoBatching(internalDSFoo, 10)}, - {Prefix: datastore.NewKey("/foo/bar"), Datastore: autobatch.NewAutoBatching(internalDSFooBar, 10)}, - {Prefix: datastore.NewKey("/"), Datastore: autobatch.NewAutoBatching(internalDSRoot, 10)}, + {Prefix: datastore.NewKey("/foo"), Datastore: autobatch.NewAutoBatching(internalDSFoo, 10, 100*time.Millisecond)}, + {Prefix: datastore.NewKey("/foo/bar"), Datastore: autobatch.NewAutoBatching(internalDSFooBar, 10, 100*time.Millisecond)}, + {Prefix: datastore.NewKey("/"), Datastore: autobatch.NewAutoBatching(internalDSRoot, 10, 100*time.Millisecond)}, }) // Testing scenarios From 83ddbeff1c89736110c7e6d3ae921773b7d71136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 3 Nov 2021 08:21:14 +0100 Subject: [PATCH 2/2] autobatch: use go-log --- autobatch/autobatch.go | 8 ++++--- go.mod | 5 +++-- go.sum | 50 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/autobatch/autobatch.go b/autobatch/autobatch.go index 4e0ceea..083d3d8 100644 --- a/autobatch/autobatch.go +++ b/autobatch/autobatch.go @@ -4,14 +4,16 @@ package autobatch import ( - "log" "sync" "time" ds "github.com/ipfs/go-datastore" dsq "github.com/ipfs/go-datastore/query" + logging "github.com/ipfs/go-log" ) +var log = logging.Logger("datastore/autobatch") + // Datastore implements a go-datastore. type Datastore struct { child ds.Batching @@ -62,12 +64,12 @@ func (d *Datastore) runBatcher() { b, err := d.prepareBatch(nil) if err != nil { - log.Println(err) + log.Error(err) return } err = b.Commit() if err != nil { - log.Println(err) + log.Error(err) return } } diff --git a/go.mod b/go.mod index 89cf50d..a868f71 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ require ( github.com/google/uuid v1.1.1 github.com/ipfs/go-detect-race v0.0.1 github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8 + github.com/ipfs/go-log v1.0.5 github.com/jbenet/goprocess v0.1.4 github.com/kr/pretty v0.2.0 // indirect - go.uber.org/multierr v1.5.0 - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 + go.uber.org/multierr v1.6.0 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 ) diff --git a/go.sum b/go.sum index a25a102..533d4e4 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -10,9 +12,14 @@ github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8 h1:NAviDvJ0WXgD+yiL2Rj35AmnfgI11+pHXbdciD917U0= github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= +github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= +github.com/ipfs/go-log/v2 v2.1.3 h1:1iS3IU7aXRlbgUpN8yTTpJ53NXYjAe37vcI5+5nYrzk= +github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= @@ -20,40 +27,71 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=