Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

329 lines
9.6 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pull
import (
"context"
"errors"
"golang.org/x/sync/errgroup"
"github.com/dolthub/dolt/go/store/hash"
)
type HasManyer interface {
HasMany(context.Context, hash.HashSet) (hash.HashSet, error)
}
type TrackerConfig struct {
HasManyer HasManyer
BatchSize int
}
const hasManyThreadCount = 3
// A PullChunkTracker keeps track of seen chunk addresses and returns every
// seen chunk address which is not already in the destination database exactly
// once. A Puller instantiantes one of these with the initial set of addresses
// to pull, and repeatedly calls |GetChunksToFetch|. It passes in all
// references it finds in the fetched chunks to |Seen|, and continues to call
// |GetChunksToFetch| and deliver new addresses to |Seen| until
// |GetChunksToFetch| returns |false| from its |more| return boolean.
//
// PullChunkTracker is able to call |HasMany| on the destination database in
// parallel with other work the Puller does and abstracts out the logic for
// keeping track of seen, unchecked and to pull hcunk addresses.
type PullChunkTracker struct {
seen hash.HashSet
cfg TrackerConfig
uncheckedCh chan hash.Hash
processedCh chan struct{}
doneCh chan struct{}
reqCh chan *trackerGetAbsentReq
}
func NewPullChunkTracker(cfg TrackerConfig) *PullChunkTracker {
ret := &PullChunkTracker{
seen: make(hash.HashSet),
cfg: cfg,
uncheckedCh: make(chan hash.Hash),
processedCh: make(chan struct{}),
doneCh: make(chan struct{}),
reqCh: make(chan *trackerGetAbsentReq),
}
return ret
}
func (t *PullChunkTracker) Run(ctx context.Context, initial hash.HashSet) error {
defer close(t.doneCh)
t.seen.InsertAll(initial)
return t.reqRespThread(ctx, initial)
}
func (t *PullChunkTracker) Seen(ctx context.Context, h hash.Hash) {
if !t.seen.Has(h) {
t.seen.Insert(h)
t.addUnchecked(ctx, h)
}
}
// Call this for every returned hash that has been successfully processed.
//
// GetChunksToFetch() requires a matching |TickProcessed| call for each
// returned Hash before it will return |hasMany == false|.
func (t *PullChunkTracker) TickProcessed(ctx context.Context) {
select {
case t.processedCh <- struct{}{}:
case <-ctx.Done():
}
}
func (t *PullChunkTracker) Close() {
close(t.uncheckedCh)
<-t.doneCh
}
func (t *PullChunkTracker) addUnchecked(ctx context.Context, h hash.Hash) {
select {
case t.uncheckedCh <- h:
case <-ctx.Done():
}
}
func (t *PullChunkTracker) GetChunksToFetch(ctx context.Context) (hash.HashSet, bool, error) {
var req trackerGetAbsentReq
req.ready = make(chan struct{})
select {
case t.reqCh <- &req:
case <-ctx.Done():
return nil, false, context.Cause(ctx)
}
select {
case <-req.ready:
case <-ctx.Done():
return nil, false, context.Cause(ctx)
}
return req.hs, req.ok, req.err
}
// The main logic of the PullChunkTracker, receives requests from other threads
// and responds to them.
func (t *PullChunkTracker) reqRespThread(ctx context.Context, initial hash.HashSet) error {
doneCh := make(chan struct{})
hasManyReqCh := make(chan trackerHasManyReq)
hasManyRespCh := make(chan trackerHasManyResp)
eg, ctx := errgroup.WithContext(ctx)
for range hasManyThreadCount {
eg.Go(func() error {
return hasManyThread(ctx, t.cfg.HasManyer, hasManyReqCh, hasManyRespCh, doneCh)
})
}
eg.Go(func() error {
defer close(doneCh)
unchecked := make([]hash.HashSet, 0)
absent := make([]hash.HashSet, 0)
var err error
outstanding := 0
unprocessed := 0
if len(initial) > 0 {
unchecked = append(unchecked, initial)
outstanding += 1
}
for {
var thisReqCh = t.reqCh
if len(absent) == 0 && (outstanding != 0 || unprocessed != 0) {
// If we are waiting for a HasMany response and we don't currently have any
// absent addresses to return, block any absent requests.
thisReqCh = nil
}
var thisHasManyReqCh chan trackerHasManyReq
var hasManyReq trackerHasManyReq
if len(unchecked) > 0 {
hasManyReq.hs = unchecked[0]
thisHasManyReqCh = hasManyReqCh
}
select {
case h, ok := <-t.uncheckedCh:
// uncheckedCh closing means the PullChunkTracker was closed.
if !ok {
return nil
}
// |h| is a hash we need to check against HasMany on the destination.
// It could become a hash we fetch from source and add to the destination
// or it could already be present.
if len(unchecked) == 0 || len(unchecked[len(unchecked)-1]) >= t.cfg.BatchSize {
outstanding += 1
unchecked = append(unchecked, make(hash.HashSet))
}
unchecked[len(unchecked)-1].Insert(h)
case resp := <-hasManyRespCh:
// A hasMany response came back.
outstanding -= 1
if resp.err != nil {
err = errors.Join(err, resp.err)
} else if len(resp.hs) > 0 {
// Add all the resp.hs hashes, those which are not already present
// in dest, to our batches of absent hashes we will return through
// GetChunksToFetch.
absent = appendAbsent(absent, resp.hs, t.cfg.BatchSize)
}
case thisHasManyReqCh <- hasManyReq:
// We delivered a hasMany request to a hasManyThread.
// Remove it here. We do not need to update |outstanding|, since
// it was updated when we created the new batch in |unchecked|.
_, unchecked = pop(unchecked)
case <-t.processedCh:
// TickProcessed helps us keep track of chunks which we returned from GetChunksToFetch
// and which are still being processed by WalkAddrs. This only gets called after all
// unchecked hashes in the chunks have been delivered to us.
unprocessed -= 1
case req := <-thisReqCh:
// A request for GetChunksToFetch.
if err != nil {
// Deliver an error we experienced. HasMany can error, and this is where
// a client sees it, for example.
req.err = err
close(req.ready)
err = nil
} else if len(absent) == 0 {
// We have no more chunks to deliver. If len(absent) == 0 but we had
// unprocessed stuff, we would have had a |nil| |thisReqCh|. The fact that
// we accepted the request means we are ready to tell the client that there
// are no more chunks to fetch.
req.ok = false
close(req.ready)
} else {
// |absent[0]| is as full a batch as we have.
req.ok = true
req.hs, absent = pop(absent)
unprocessed += len(req.hs)
close(req.ready)
}
case <-ctx.Done():
return context.Cause(ctx)
}
}
})
return eg.Wait()
}
// pop returns the first element of s and the remaining elements of
// s. It copies any suffix to the front of |s| and nils the last
// element of |s| so that memory doesn't leak through |s[1:]| retaining
// s[0].
func pop[T any](s []T) (T, []T) {
ret := s[0]
copy(s[:], s[1:])
var empty T
s[len(s)-1] = empty
return ret, s[:len(s)-1]
}
// appendAbsent adds all elements in |toadd| to HashSets at the end of |absent|. It creates new HashSets
// at the end of |absent| if inserting into an existing set would cause its size to exceed |sz|.
//
// As a special case, |appendAbsent| just returns []hash.HashSet{toadd} if |absent| is already empty.
func appendAbsent(absent []hash.HashSet, toadd hash.HashSet, sz int) []hash.HashSet {
if len(toadd) == 0 {
return absent
}
// Don't bother splitting up toadd to |sz| if it is the first batch.
if len(absent) == 0 {
absent = append(absent, toadd)
return absent
}
for h := range toadd {
if len(absent[len(absent)-1]) >= sz {
absent = append(absent, make(hash.HashSet))
}
absent[len(absent)-1].Insert(h)
}
return absent
}
// Run by a PullChunkTracker, calls HasMany on a batch of addresses and delivers the results.
func hasManyThread(ctx context.Context, hasManyer HasManyer, reqCh <-chan trackerHasManyReq, respCh chan<- trackerHasManyResp, doneCh <-chan struct{}) error {
for {
select {
case req := <-reqCh:
hs, err := hasManyer.HasMany(ctx, req.hs)
if err != nil {
select {
case respCh <- trackerHasManyResp{err: err}:
case <-ctx.Done():
return context.Cause(ctx)
case <-doneCh:
return nil
}
} else {
select {
case respCh <- trackerHasManyResp{hs: hs}:
case <-ctx.Done():
return context.Cause(ctx)
case <-doneCh:
return nil
}
}
case <-doneCh:
return nil
case <-ctx.Done():
return context.Cause(ctx)
}
}
}
// Sent by the tracker thread to a HasMany thread, includes a batch of
// addresses to HasMany. The response comes back to the tracker thread on a
// separate channel as a |trackerHasManyResp|.
type trackerHasManyReq struct {
hs hash.HashSet
}
// Sent by the HasMany thread back to the tracker thread.
// If HasMany returned an error, it will be returned here.
type trackerHasManyResp struct {
hs hash.HashSet
err error
}
// Sent by a client calling |GetChunksToFetch| to the tracker thread. The
// tracker thread will return a batch of chunk addresses that need to be
// fetched from source and added to destination.
//
// This will block until HasMany requests are completed.
//
// If |ok| is |false|, then the Tracker is closing because every absent address
// has been delivered.
type trackerGetAbsentReq struct {
err error
hs hash.HashSet
ready chan struct{}
ok bool
}