Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
module github.com/libp2p/go-libp2p-kad-dht

go 1.24
go 1.24.0

require (
github.com/gammazero/deque v1.0.0
github.com/gammazero/deque v1.1.0
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
github.com/guillaumemichel/reservedpool v0.2.0
github.com/hashicorp/golang-lru v1.0.2
github.com/ipfs/boxo v0.33.1
github.com/ipfs/go-cid v0.5.0
github.com/ipfs/go-datastore v0.8.2
github.com/ipfs/go-datastore v0.8.3
github.com/ipfs/go-detect-race v0.0.1
github.com/ipfs/go-log/v2 v2.8.0
github.com/ipfs/go-dsqueue v0.0.4
github.com/ipfs/go-log/v2 v2.8.1
github.com/ipfs/go-test v0.2.3
github.com/libp2p/go-libp2p v0.43.0
github.com/libp2p/go-libp2p-kbucket v0.8.0
Expand Down Expand Up @@ -53,6 +54,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/ipfs/go-block-format v0.2.2 // indirect
github.com/ipld/go-ipld-prime v0.21.0 // indirect
Expand Down
16 changes: 10 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
github.com/gammazero/deque v1.1.0 h1:OyiyReBbnEG2PP0Bnv1AASLIYvyKqIFN5xfl1t8oGLo=
github.com/gammazero/deque v1.1.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
Expand Down Expand Up @@ -117,6 +117,8 @@ github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmv
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
Expand All @@ -131,17 +133,19 @@ github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
github.com/ipfs/go-datastore v0.1.0/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE=
github.com/ipfs/go-datastore v0.1.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw=
github.com/ipfs/go-datastore v0.8.2 h1:Jy3wjqQR6sg/LhyY0NIePZC3Vux19nLtg7dx0TVqr6U=
github.com/ipfs/go-datastore v0.8.2/go.mod h1:W+pI1NsUsz3tcsAACMtfC+IZdnQTnC/7VfPoJBQuts0=
github.com/ipfs/go-datastore v0.8.3 h1:z391GsQyGKUIUof2tPoaZVeDknbt7fNHs6Gqjcw5Jo4=
github.com/ipfs/go-datastore v0.8.3/go.mod h1:raxQ/CreIy9L6MxT71ItfMX12/ASN6EhXJoUFjICQ2M=
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ds-badger v0.0.7/go.mod h1:qt0/fWzZDoPW6jpQeqUjR5kBfhDNB65jd9YlmAvpQBk=
github.com/ipfs/go-ds-leveldb v0.1.0/go.mod h1:hqAW8y4bwX5LWcCtku2rFNX3vjDZCy5LZCg+cSZvYb8=
github.com/ipfs/go-dsqueue v0.0.4 h1:tesq26hKRYPG72Tu9kZKsbsLWp1KBfAxWNQlMyU17tk=
github.com/ipfs/go-dsqueue v0.0.4/go.mod h1:K68ng9BVl+gLr8fqCJKaoXnXqo6MzQ6nV0MhZZFEwg4=
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
github.com/ipfs/go-log/v2 v2.8.0 h1:SptNTPJQV3s5EF4FdrTu/yVdOKfGbDgn1EBZx4til2o=
github.com/ipfs/go-log/v2 v2.8.0/go.mod h1:2LEEhdv8BGubPeSFTyzbqhCqrwqxCbuTNTLWqgNAipo=
github.com/ipfs/go-log/v2 v2.8.1 h1:Y/X36z7ASoLJaYIJAL4xITXgwf7RVeqb1+/25aq/Xk0=
github.com/ipfs/go-log/v2 v2.8.1/go.mod h1:NyhTBcZmh2Y55eWVjOeKf8M7e4pnJYM3yDZNxQBWEEY=
github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc=
github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o=
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
Expand Down
65 changes: 65 additions & 0 deletions provider/buffered/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Package buffered provides a buffered provider implementation that queues operations
// and processes them in batches for improved performance.
package buffered

import "time"

const (
// DefaultDsName is the default datastore namespace for the buffered provider.
DefaultDsName = "bprov" // for buffered provider
// DefaultBatchSize is the default number of operations to process in a single batch.
DefaultBatchSize = 1 << 10
// DefaultIdleWriteTime is the default duration to wait before flushing pending operations.
DefaultIdleWriteTime = time.Minute
)

// config contains all options for the buffered provider.
type config struct {
dsName string
batchSize int
idleWriteTime time.Duration
}

// Option is a function that configures the buffered provider.
type Option func(*config)

// getOpts creates a config and applies Options to it.
func getOpts(opts []Option) config {
cfg := config{
dsName: DefaultDsName,
batchSize: DefaultBatchSize,
idleWriteTime: DefaultIdleWriteTime,
}

for _, opt := range opts {
opt(&cfg)
}
return cfg
}

// WithDsName sets the datastore namespace for the buffered provider.
// If name is empty, the option is ignored.
func WithDsName(name string) Option {
return func(c *config) {
if len(name) > 0 {
c.dsName = name
}
}
}

// WithBatchSize sets the number of operations to process in a single batch.
// If n is zero or negative, the option is ignored.
func WithBatchSize(n int) Option {
return func(c *config) {
if n > 0 {
c.batchSize = n
}
}
}

// WithIdleWriteTime sets the duration to wait before flushing pending operations.
func WithIdleWriteTime(d time.Duration) Option {
return func(c *config) {
c.idleWriteTime = d
}
}
244 changes: 244 additions & 0 deletions provider/buffered/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package buffered

import (
"errors"
"sync"

"github.com/ipfs/go-datastore"
"github.com/ipfs/go-dsqueue"
logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-libp2p-kad-dht/provider"
"github.com/libp2p/go-libp2p-kad-dht/provider/internal"
mh "github.com/multiformats/go-multihash"
)

var logger = logging.Logger(provider.LoggerName)

const (
// provideOnceOp represents a one-time provide operation.
provideOnceOp byte = iota
// startProvidingOp represents starting continuous providing.
startProvidingOp
// forceStartProvidingOp represents forcefully starting providing (overrides existing).
forceStartProvidingOp
// stopProvidingOp represents stopping providing.
stopProvidingOp
// lastOp is used for array sizing.
lastOp
)

var _ internal.Provider = (*SweepingProvider)(nil)

// SweepingProvider implements a buffered provider that queues operations and
// processes them asynchronously in batches.
type SweepingProvider struct {
closeOnce sync.Once
done chan struct{}
closed chan struct{}
provider internal.Provider
queue *dsqueue.DSQueue
batchSize int
}

// New creates a new SweepingProvider that wraps the given provider with
// buffering capabilities. Operations are queued and processed asynchronously
// in batches for improved performance.
func New(prov internal.Provider, ds datastore.Batching, opts ...Option) *SweepingProvider {
cfg := getOpts(opts)
s := &SweepingProvider{
done: make(chan struct{}),
closed: make(chan struct{}),

provider: prov,
queue: dsqueue.New(ds, cfg.dsName,
dsqueue.WithDedupCacheSize(0), // disable deduplication
dsqueue.WithIdleWriteTime(cfg.idleWriteTime),
),
batchSize: cfg.batchSize,
}
go s.worker()
return s
}

// Close stops the provider and releases all resources.
//
// It waits for the worker goroutine to finish processing current operations
// and closes the underneath provider. The queue current state is persisted on
// the datastore.
func (s *SweepingProvider) Close() error {
var err error
s.closeOnce.Do(func() {
close(s.closed)
err = errors.Join(s.queue.Close(), s.provider.Close())
<-s.done
})
return err
}

// toBytes serializes an operation and multihash into a byte slice for storage.
func toBytes(op byte, key mh.Multihash) []byte {
return append([]byte{op}, key...)
}

// fromBytes deserializes a byte slice back into an operation and multihash.
func fromBytes(data []byte) (byte, mh.Multihash, error) {
op := data[0]
h, err := mh.Cast(data[1:])
return op, h, err
}

// getOperations processes a batch of dequeued operations and groups them by
// type.
//
// It discards multihashes from the `StopProviding` operation if
// `StartProviding` was called after `StopProviding` for the same multihash.
func getOperations(dequeued [][]byte) ([][]mh.Multihash, error) {
ops := [lastOp][]mh.Multihash{}
stopProv := make(map[string]struct{})

for _, bs := range dequeued {
op, h, err := fromBytes(bs)
if err != nil {
return nil, err
}
switch op {
case provideOnceOp:
ops[provideOnceOp] = append(ops[provideOnceOp], h)
case startProvidingOp, forceStartProvidingOp:
delete(stopProv, string(h))
ops[op] = append(ops[op], h)
case stopProvidingOp:
stopProv[string(h)] = struct{}{}
}
}
for hstr := range stopProv {
ops[stopProvidingOp] = append(ops[stopProvidingOp], mh.Multihash(hstr))
}
return ops[:], nil
}

// worker processes operations from the queue in batches.
// It runs in a separate goroutine and continues until the provider is closed.
func (s *SweepingProvider) worker() {
defer close(s.done)
for {
select {
case <-s.closed:
return
default:
}

res, err := s.queue.GetN(s.batchSize)
if err != nil {
logger.Warnf("BufferedSweepingProvider unable to dequeue: %v", err)
continue
}
ops, err := getOperations(res)
if err != nil {
logger.Warnf("BufferedSweepingProvider unable to parse dequeued item: %v", err)
continue
}

// Process `StartProviding` (force=true) ops first, so that if
// `StartProviding` (force=false) is called after, there is no need to
// enqueue the multihash a second time to the provide queue.
err = s.provider.StartProviding(true, ops[forceStartProvidingOp]...)
if err != nil {
logger.Warnf("BufferedSweepingProvider unable to start providing (force): %v", err)
}
err = s.provider.StartProviding(false, ops[startProvidingOp]...)
if err != nil {
logger.Warnf("BufferedSweepingProvider unable to start providing: %v", err)
}
err = s.provider.ProvideOnce(ops[provideOnceOp]...)
if err != nil {
logger.Warnf("BufferedSweepingProvider unable to provide once: %v", err)
}
// Process `StopProviding` last, so that multihashes that should have been
// provided, and then stopped provided in the same batch are provided only
// once. Don't `StopProviding` multihashes, for which `StartProviding` has
// been called after `StopProviding`.
err = s.provider.StopProviding(ops[stopProvidingOp]...)
if err != nil {
logger.Warnf("BufferedSweepingProvider unable to stop providing: %v", err)
}
}
}

// enqueue adds operations to the queue for asynchronous processing.
func (s *SweepingProvider) enqueue(op byte, keys ...mh.Multihash) error {
for _, h := range keys {
if err := s.queue.Put(toBytes(op, h)); err != nil {
return err
}
}
return nil
}

// ProvideOnce enqueues multihashes for which the provider will send provider
// records out only once to the DHT swarm. It does NOT take the responsibility
// to reprovide these keys.
//
// Returns immediately after enqueuing the keys, the actual provide operation
// happens asynchronously. Returns an error if the multihashes couldn't be
// enqueued.
func (s *SweepingProvider) ProvideOnce(keys ...mh.Multihash) error {
return s.enqueue(provideOnceOp, keys...)
}

// StartProviding adds the supplied keys to the queue of keys that will be
// provided to the DHT swarm unless they were already provided in the past. The
// keys will be periodically reprovided until StopProviding is called for the
// same keys or the keys are removed from the Keystore.
//
// If force is true, the keys are provided to the DHT swarm regardless of
// whether they were already being reprovided in the past.
//
// Returns immediately after enqueuing the keys, the actual provide operation
// happens asynchronously. Returns an error if the multihashes couldn't be
// enqueued.
func (s *SweepingProvider) StartProviding(force bool, keys ...mh.Multihash) error {
op := startProvidingOp
if force {
op = forceStartProvidingOp
}
return s.enqueue(op, keys...)
}

// StopProviding adds the supplied multihashes to the BufferedSweepingProvider
// queue, to stop reproviding the given keys to the DHT swarm.
//
// The node stops being referred as a provider when the provider records in the
// DHT swarm expire.
//
// Returns immediately after enqueuing the keys, the actual provide operation
// happens asynchronously. Returns an error if the multihashes couldn't be
// enqueued.
func (s *SweepingProvider) StopProviding(keys ...mh.Multihash) error {
return s.enqueue(stopProvidingOp, keys...)
}

// Clear clears the all the keys from the provide queue and returns the number
// of keys that were cleared.
//
// The keys are not deleted from the keystore, so they will continue to be
// reprovided as scheduled.
func (s *SweepingProvider) Clear() int {
return s.provider.Clear()
}

// RefreshSchedule scans the KeyStore for any keys that are not currently
// scheduled for reproviding. If such keys are found, it schedules their
// associated keyspace region to be reprovided.
//
// This function doesn't remove prefixes that have no keys from the schedule.
// This is done automatically during the reprovide operation if a region has no
// keys.
//
// Returns an error if the provider is closed or if the node is currently
// Offline (either never bootstrapped, or disconnected since more than
// `OfflineDelay`). The schedule depends on the network size, hence recent
// network connectivity is essential.
func (s *SweepingProvider) RefreshSchedule() error {
return s.provider.RefreshSchedule()
}
Loading
Loading