fd55dad735
It has been noted in the forum that, during a Retain operation, when a piece can't be deleted, the process never completes. The error is written to the log, but the completion line "Moved pieces to trash during retain" never is. This `return` line is the reason. We should instead continue the loop. Change-Id: I0f51d34aba0e81ad60a75802069b42dc135ad907 Refs: https://github.com/storj/storj/issues/6482
371 lines
9.9 KiB
Go
371 lines
9.9 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package retain
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
|
"github.com/zeebo/errs"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"storj.io/common/bloomfilter"
|
|
"storj.io/common/storj"
|
|
"storj.io/storj/storagenode/pieces"
|
|
)
|
|
|
|
var (
|
|
mon = monkit.Package()
|
|
|
|
// Error is the default error class for retain errors.
|
|
Error = errs.Class("retain")
|
|
)
|
|
|
|
// Config defines parameters for the retain service.
|
|
type Config struct {
|
|
MaxTimeSkew time.Duration `help:"allows for small differences in the satellite and storagenode clocks" default:"72h0m0s"`
|
|
Status Status `help:"allows configuration to enable, disable, or test retain requests from the satellite. Options: (disabled/enabled/debug)" default:"enabled"`
|
|
Concurrency int `help:"how many concurrent retain requests can be processed at the same time." default:"5"`
|
|
}
|
|
|
|
// Request contains all the info necessary to process a retain request.
|
|
type Request struct {
|
|
SatelliteID storj.NodeID
|
|
CreatedBefore time.Time
|
|
Filter *bloomfilter.Filter
|
|
}
|
|
|
|
// Status is a type defining the enabled/disabled status of retain requests.
|
|
type Status uint32
|
|
|
|
const (
|
|
// Disabled means we do not do anything with retain requests.
|
|
Disabled Status = iota + 1
|
|
// Enabled means we fully enable retain requests and delete data not defined by bloom filter.
|
|
Enabled
|
|
// Debug means we partially enable retain requests, and print out pieces we should delete, without actually deleting them.
|
|
Debug
|
|
)
|
|
|
|
// Set implements pflag.Value.
|
|
func (v *Status) Set(s string) error {
|
|
switch s {
|
|
case "disabled":
|
|
*v = Disabled
|
|
case "enabled":
|
|
*v = Enabled
|
|
case "debug":
|
|
*v = Debug
|
|
default:
|
|
return Error.New("invalid status %q", s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Type implements pflag.Value.
|
|
func (*Status) Type() string { return "storj.Status" }
|
|
|
|
// String implements pflag.Value.
|
|
func (v *Status) String() string {
|
|
switch *v {
|
|
case Disabled:
|
|
return "disabled"
|
|
case Enabled:
|
|
return "enabled"
|
|
case Debug:
|
|
return "debug"
|
|
default:
|
|
return "invalid"
|
|
}
|
|
}
|
|
|
|
// Service queues and processes retain requests from satellites.
|
|
//
|
|
// architecture: Worker
|
|
type Service struct {
|
|
log *zap.Logger
|
|
config Config
|
|
|
|
cond sync.Cond
|
|
queued map[storj.NodeID]Request
|
|
working map[storj.NodeID]struct{}
|
|
group errgroup.Group
|
|
|
|
closedOnce sync.Once
|
|
closed chan struct{}
|
|
started bool
|
|
|
|
store *pieces.Store
|
|
}
|
|
|
|
// NewService creates a new retain service.
|
|
func NewService(log *zap.Logger, store *pieces.Store, config Config) *Service {
|
|
return &Service{
|
|
log: log,
|
|
config: config,
|
|
|
|
cond: *sync.NewCond(&sync.Mutex{}),
|
|
queued: make(map[storj.NodeID]Request),
|
|
working: make(map[storj.NodeID]struct{}),
|
|
closed: make(chan struct{}),
|
|
|
|
store: store,
|
|
}
|
|
}
|
|
|
|
// Queue adds a retain request to the queue.
|
|
// It discards a request for a satellite that already has a queued request.
|
|
// true is returned if the request is queued and false is returned if it is discarded.
|
|
func (s *Service) Queue(req Request) bool {
|
|
s.cond.L.Lock()
|
|
defer s.cond.L.Unlock()
|
|
|
|
select {
|
|
case <-s.closed:
|
|
return false
|
|
default:
|
|
}
|
|
|
|
s.queued[req.SatelliteID] = req
|
|
s.cond.Broadcast()
|
|
|
|
return true
|
|
}
|
|
|
|
// Run listens for queued retain requests and processes them as they come in.
|
|
func (s *Service) Run(ctx context.Context) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
// Hold the lock while we spawn the workers because a concurrent Close call
|
|
// can race and wait for them. We later temporarily drop the lock while we
|
|
// wait for the workers to exit.
|
|
s.cond.L.Lock()
|
|
defer s.cond.L.Unlock()
|
|
|
|
// Ensure Run is only ever called once. If not, there's many subtle
|
|
// bugs with concurrency.
|
|
if s.started {
|
|
return Error.New("service already started")
|
|
}
|
|
s.started = true
|
|
|
|
// Ensure Run doesn't start after it's closed. Then we may leak some
|
|
// workers.
|
|
select {
|
|
case <-s.closed:
|
|
return Error.New("service Closed")
|
|
default:
|
|
}
|
|
|
|
// Create a sub-context that we can cancel.
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Start a goroutine that does most of the work of Close in the case
|
|
// the context is canceled and broadcasts to anyone waiting on the condition
|
|
// variable to check the state.
|
|
s.group.Go(func() error {
|
|
defer s.cond.Broadcast()
|
|
defer cancel()
|
|
|
|
select {
|
|
case <-s.closed:
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
s.cond.L.Lock()
|
|
s.closedOnce.Do(func() { close(s.closed) })
|
|
s.cond.L.Unlock()
|
|
|
|
return ctx.Err()
|
|
}
|
|
})
|
|
|
|
for i := 0; i < s.config.Concurrency; i++ {
|
|
s.group.Go(func() error {
|
|
// Grab lock to check things.
|
|
s.cond.L.Lock()
|
|
defer s.cond.L.Unlock()
|
|
|
|
for {
|
|
// If we have closed, exit.
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-s.closed:
|
|
return nil
|
|
default:
|
|
}
|
|
|
|
// Grab next item from queue.
|
|
request, ok := s.next()
|
|
if !ok {
|
|
// Nothing in queue, go to sleep and wait for
|
|
// things shutting down or next item.
|
|
s.cond.Wait()
|
|
continue
|
|
}
|
|
|
|
// Temporarily Unlock so others can work on the queue while
|
|
// we're working on the retain request.
|
|
s.cond.L.Unlock()
|
|
|
|
// Signal to anyone waiting that the queue state changed.
|
|
s.cond.Broadcast()
|
|
|
|
// Run retaining process.
|
|
err := s.retainPieces(ctx, request)
|
|
if err != nil {
|
|
s.log.Error("retain pieces failed", zap.Error(err))
|
|
}
|
|
|
|
// Mark the request as finished. Relock to maintain that
|
|
// at the top of the for loop the lock is held.
|
|
s.cond.L.Lock()
|
|
s.finish(request)
|
|
s.cond.Broadcast()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Unlock while we wait for the workers to exit.
|
|
s.cond.L.Unlock()
|
|
err = s.group.Wait()
|
|
s.cond.L.Lock()
|
|
|
|
// Clear the queue after Wait has exited. We're sure no more entries
|
|
// can be added after we acquire the mutex because wait spawned a
|
|
// worker that ensures the closed channel is closed before it exits.
|
|
s.queued = nil
|
|
s.cond.Broadcast()
|
|
|
|
return err
|
|
}
|
|
|
|
// next returns next item from queue, requires mutex to be held.
|
|
func (s *Service) next() (Request, bool) {
|
|
for id, request := range s.queued {
|
|
// Check whether a worker is retaining this satellite,
|
|
// if, yes, then try to get something else from the queue.
|
|
if _, ok := s.working[request.SatelliteID]; ok {
|
|
continue
|
|
}
|
|
delete(s.queued, id)
|
|
// Mark this satellite as being worked on.
|
|
s.working[request.SatelliteID] = struct{}{}
|
|
return request, true
|
|
}
|
|
return Request{}, false
|
|
}
|
|
|
|
// finish marks the request as finished, requires mutex to be held.
|
|
func (s *Service) finish(request Request) {
|
|
delete(s.working, request.SatelliteID)
|
|
}
|
|
|
|
// Close causes any pending Run to exit and waits for any retain requests to
|
|
// clean up.
|
|
func (s *Service) Close() error {
|
|
s.cond.L.Lock()
|
|
s.closedOnce.Do(func() { close(s.closed) })
|
|
s.cond.L.Unlock()
|
|
|
|
s.cond.Broadcast()
|
|
// ignoring error here, because the same error is already returned from Run.
|
|
_ = s.group.Wait()
|
|
return nil
|
|
}
|
|
|
|
// TestWaitUntilEmpty blocks until the queue and working is empty.
|
|
// When Run exits, it empties the queue.
|
|
func (s *Service) TestWaitUntilEmpty() {
|
|
s.cond.L.Lock()
|
|
defer s.cond.L.Unlock()
|
|
|
|
for len(s.queued) > 0 || len(s.working) > 0 {
|
|
s.cond.Wait()
|
|
}
|
|
}
|
|
|
|
// Status returns the retain status.
|
|
func (s *Service) Status() Status {
|
|
return s.config.Status
|
|
}
|
|
|
|
func (s *Service) retainPieces(ctx context.Context, req Request) (err error) {
|
|
// if retain status is disabled, return immediately
|
|
if s.config.Status == Disabled {
|
|
return nil
|
|
}
|
|
|
|
defer mon.Task()(&ctx, req.SatelliteID, req.CreatedBefore)(&err)
|
|
|
|
numDeleted := 0
|
|
satelliteID := req.SatelliteID
|
|
filter := req.Filter
|
|
|
|
// subtract some time to leave room for clock difference between the satellite and storage node
|
|
createdBefore := req.CreatedBefore.Add(-s.config.MaxTimeSkew)
|
|
started := time.Now().UTC()
|
|
filterHashCount, _ := req.Filter.Parameters()
|
|
mon.IntVal("garbage_collection_created_before").Observe(createdBefore.Unix())
|
|
mon.IntVal("garbage_collection_filter_hash_count").Observe(int64(filterHashCount))
|
|
mon.IntVal("garbage_collection_filter_size").Observe(filter.Size())
|
|
mon.IntVal("garbage_collection_started").Observe(started.Unix())
|
|
|
|
s.log.Info("Prepared to run a Retain request.",
|
|
zap.Time("Created Before", createdBefore),
|
|
zap.Int64("Filter Size", filter.Size()),
|
|
zap.Stringer("Satellite ID", satelliteID))
|
|
|
|
pieceIDs, piecesCount, piecesSkipped, err := s.store.SatellitePiecesToTrash(ctx, satelliteID, createdBefore, filter)
|
|
if err != nil {
|
|
return Error.Wrap(err)
|
|
}
|
|
|
|
piecesToDeleteCount := len(pieceIDs)
|
|
|
|
for i := range pieceIDs {
|
|
pieceID := pieceIDs[i]
|
|
s.log.Debug("About to move piece to trash",
|
|
zap.Stringer("Satellite ID", satelliteID),
|
|
zap.Stringer("Piece ID", pieceID),
|
|
zap.String("Status", s.config.Status.String()))
|
|
|
|
// if retain status is enabled, delete pieceid
|
|
if s.config.Status == Enabled {
|
|
if err = s.trash(ctx, satelliteID, pieceID); err != nil {
|
|
s.log.Warn("failed to delete piece",
|
|
zap.Stringer("Satellite ID", satelliteID),
|
|
zap.Stringer("Piece ID", pieceID),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
}
|
|
numDeleted++
|
|
}
|
|
mon.IntVal("garbage_collection_pieces_count").Observe(piecesCount)
|
|
mon.IntVal("garbage_collection_pieces_skipped").Observe(piecesSkipped)
|
|
mon.IntVal("garbage_collection_pieces_to_delete_count").Observe(int64(piecesToDeleteCount))
|
|
mon.IntVal("garbage_collection_pieces_deleted").Observe(int64(numDeleted))
|
|
mon.DurationVal("garbage_collection_loop_duration").Observe(time.Now().UTC().Sub(started))
|
|
s.log.Info("Moved pieces to trash during retain", zap.Int("num deleted", numDeleted), zap.String("Retain Status", s.config.Status.String()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// trash wraps retains piece deletion to monitor moving retained piece to trash error during garbage collection.
|
|
func (s *Service) trash(ctx context.Context, satelliteID storj.NodeID, pieceID storj.PieceID) (err error) {
|
|
defer mon.Task()(&ctx, satelliteID)(&err)
|
|
return s.store.Trash(ctx, satelliteID, pieceID)
|
|
}
|
|
|
|
// HowManyQueued peeks at the number of bloom filters queued.
|
|
func (s *Service) HowManyQueued() int {
|
|
return len(s.queued)
|
|
}
|