01f7a9e4e8
This commit attempts to fix race issues in the maptracker since the introduction of the OperationTracker. There were two main problems: * Duplicity tracking the state both in the state map and the opTracker * Non atomiciy of operations with different threads being able to affect other threads operations. A test performing random Track/Untracks on the same Cid quickly showed that items would sometimes stay as pin_queued or pin_unqueued. That happened because operations could be cancelled under the hood by a different request, while leaving the map status untouched. It was not simply to deal with this issues without a refactoring. First, the state map has been removed, and the operation tracker now provides status information for any Cid. This implies that the tracker keeps all operations and operations have a `PhaseDone`. There's also a new `OperationRemote` type. Secondly, operations are only created in the tracker and can only be removed by their creators (they can be overwritten by other operations though). Operations cannot be accessed directly and modifications are limited to setting Error for PhaseDone operations. After created, *Operations are queued in the pinWorker queues which handle any status updates. This means, that, even when an operation has been removed from the tracker, status updates will not interfere with any other newer operations. In the maptracker, only the Unpin worker Cleans operations once processed. A sucessful unpin is the only way that a delete() happens in the tracker map. Otherwise, operations stay there until a newer operation for the Cid arrives and 1) cancels the existing one 2) takes its place. The tracker refuses to create a new operation if a similar "ongoing" operation of the same type exists. The final change is that Recover and RecoverAll() are not async and play by the same rules as Track() and Untrack(), queueing the items to be recovered. Note: for stateless pintracker, the tracker will need to Clean() operation of type OperationPin as well, and complement the Status reported by the tracker with those coming from IPFS. License: MIT Signed-off-by: Hector Sanjuan <code@hector.link>
193 lines
4.3 KiB
Go
193 lines
4.3 KiB
Go
package optracker
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
cid "github.com/ipfs/go-cid"
|
|
|
|
"github.com/ipfs/ipfs-cluster/api"
|
|
)
|
|
|
|
//go:generate stringer -type=OperationType
|
|
|
|
// OperationType represents the kinds of operations that the PinTracker
|
|
// performs and the operationTracker tracks the status of.
|
|
type OperationType int
|
|
|
|
const (
|
|
// OperationUnknown represents an unknown operation.
|
|
OperationUnknown OperationType = iota
|
|
// OperationPin represents a pin operation.
|
|
OperationPin
|
|
// OperationUnpin represents an unpin operation.
|
|
OperationUnpin
|
|
// OperationRemote represents an noop operation
|
|
OperationRemote
|
|
)
|
|
|
|
//go:generate stringer -type=Phase
|
|
|
|
// Phase represents the multiple phase that an operation can be in.
|
|
type Phase int
|
|
|
|
const (
|
|
// PhaseError represents an error state.
|
|
PhaseError Phase = iota
|
|
// PhaseQueued represents the queued phase of an operation.
|
|
PhaseQueued
|
|
// PhaseInProgress represents the operation as in progress.
|
|
PhaseInProgress
|
|
// PhaseDone represents the operation once finished.
|
|
PhaseDone
|
|
)
|
|
|
|
// Operation represents an ongoing operation involving a
|
|
// particular Cid. It provides the type and phase of operation
|
|
// and a way to mark the operation finished (also used to cancel).
|
|
type Operation struct {
|
|
ctx context.Context
|
|
cancel func()
|
|
|
|
mu sync.RWMutex
|
|
|
|
opType OperationType
|
|
phase Phase
|
|
ts time.Time
|
|
pin api.Pin
|
|
error string
|
|
}
|
|
|
|
// NewOperation creates a new Operation.
|
|
func NewOperation(ctx context.Context, pin api.Pin, typ OperationType, ph Phase) *Operation {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
return &Operation{
|
|
ctx: ctx,
|
|
cancel: cancel, // use *OperationTracker.Finish() instead
|
|
|
|
pin: pin,
|
|
opType: typ,
|
|
phase: ph,
|
|
ts: time.Now(),
|
|
error: "",
|
|
}
|
|
}
|
|
|
|
// Cid returns the Cid associated to this operation.
|
|
func (op *Operation) Cid() *cid.Cid {
|
|
op.mu.RLock()
|
|
defer op.mu.RUnlock()
|
|
return op.pin.Cid
|
|
}
|
|
|
|
// Context returns the context associated to this operation.
|
|
func (op *Operation) Context() context.Context {
|
|
return op.ctx
|
|
}
|
|
|
|
// Cancel will cancel the context associated to this operation.
|
|
func (op *Operation) Cancel() {
|
|
op.cancel()
|
|
}
|
|
|
|
// Phase returns the Phase.
|
|
func (op *Operation) Phase() Phase {
|
|
op.mu.RLock()
|
|
defer op.mu.RUnlock()
|
|
return op.phase
|
|
}
|
|
|
|
// SetPhase changes the Phase and updates the timestamp.
|
|
func (op *Operation) SetPhase(ph Phase) {
|
|
op.mu.Lock()
|
|
defer op.mu.Unlock()
|
|
op.phase = ph
|
|
op.ts = time.Now()
|
|
}
|
|
|
|
// Error returns any error message attached to the operation.
|
|
func (op *Operation) Error() string {
|
|
op.mu.RLock()
|
|
defer op.mu.RUnlock()
|
|
return op.error
|
|
}
|
|
|
|
// SetError sets an error message.
|
|
// It updates the timestamp, but does not update the phase.
|
|
func (op *Operation) SetError(err error) {
|
|
op.mu.Lock()
|
|
defer op.mu.Unlock()
|
|
op.error = err.Error()
|
|
op.ts = time.Now()
|
|
}
|
|
|
|
// Type returns the operation Type.
|
|
func (op *Operation) Type() OperationType {
|
|
return op.opType
|
|
}
|
|
|
|
// Pin returns the Pin object associated to the operation.
|
|
func (op *Operation) Pin() api.Pin {
|
|
return op.pin
|
|
}
|
|
|
|
// Timestamp returns the time when this operation was
|
|
// last modified (phase changed, error was set...)
|
|
func (op *Operation) Timestamp() time.Time {
|
|
op.mu.RLock()
|
|
defer op.mu.RUnlock()
|
|
return op.ts
|
|
}
|
|
|
|
// Cancelled returns whether the context for this
|
|
// operation has been cancelled.
|
|
func (op *Operation) Cancelled() bool {
|
|
select {
|
|
case <-op.ctx.Done():
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ToTrackerStatus returns an api.TrackerStatus reflecting
|
|
// the current status of this operation. It's a translation
|
|
// from the Type and the Phase.
|
|
func (op *Operation) ToTrackerStatus() api.TrackerStatus {
|
|
typ := op.Type()
|
|
ph := op.Phase()
|
|
switch typ {
|
|
case OperationPin:
|
|
switch ph {
|
|
case PhaseError:
|
|
return api.TrackerStatusPinError
|
|
case PhaseQueued:
|
|
return api.TrackerStatusPinQueued
|
|
case PhaseInProgress:
|
|
return api.TrackerStatusPinning
|
|
case PhaseDone:
|
|
return api.TrackerStatusPinned
|
|
default:
|
|
return api.TrackerStatusBug
|
|
}
|
|
case OperationUnpin:
|
|
switch ph {
|
|
case PhaseError:
|
|
return api.TrackerStatusUnpinError
|
|
case PhaseQueued:
|
|
return api.TrackerStatusUnpinQueued
|
|
case PhaseInProgress:
|
|
return api.TrackerStatusUnpinning
|
|
case PhaseDone:
|
|
return api.TrackerStatusUnpinned
|
|
default:
|
|
return api.TrackerStatusBug
|
|
}
|
|
case OperationRemote:
|
|
return api.TrackerStatusRemote
|
|
default:
|
|
return api.TrackerStatusBug
|
|
}
|
|
}
|