kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
161 行
3.8 KiB
Go
161 行
3.8 KiB
Go
package remotesync
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"sync"
|
|
)
|
|
|
|
type cleanupRetrier interface {
|
|
RetryCleanup() error
|
|
}
|
|
|
|
// PendingCleanupError reports that a cleanup retained from an earlier HTTP
|
|
// sync still owns resources, so the requested sync callback did not run.
|
|
// Err preserves the original operation and cleanup error chain.
|
|
type PendingCleanupError struct {
|
|
Err error
|
|
}
|
|
|
|
func (e *PendingCleanupError) Error() string {
|
|
if e == nil || e.Err == nil {
|
|
return "pending HTTP sync cleanup still failed"
|
|
}
|
|
return "pending HTTP sync cleanup still failed: " + e.Err.Error()
|
|
}
|
|
|
|
func (e *PendingCleanupError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Err
|
|
}
|
|
|
|
// CleanupRegistry serializes HTTP sync work and retains at most one aggregate
|
|
// of failed cleanup owners. Every retained owner must release before later
|
|
// work can start, so ownership cannot be overwritten or grow without bound.
|
|
type CleanupRegistry struct {
|
|
mu sync.Mutex
|
|
pending error
|
|
}
|
|
|
|
// Run retries cleanup errors before they can be converted to strings or
|
|
// discarded by a caller. The original error is returned unchanged so its
|
|
// operation and cleanup causes remain available to errors.Is and errors.As.
|
|
func (r *CleanupRegistry) Run(
|
|
run func() (SyncStats, error),
|
|
) (SyncStats, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if r.pending != nil {
|
|
if pending := retryCleanup(r.pending); pending != nil {
|
|
r.pending = pending
|
|
return SyncStats{}, &PendingCleanupError{Err: r.pending}
|
|
}
|
|
r.pending = nil
|
|
}
|
|
|
|
stats, err := run()
|
|
if err == nil {
|
|
return stats, nil
|
|
}
|
|
if cleanupErr := retryCleanup(err); cleanupErr != nil {
|
|
r.pending = cleanupErr
|
|
}
|
|
return stats, err
|
|
}
|
|
|
|
func retryCleanup(err error) error {
|
|
if retained, ok := err.(*retainedCleanupError); ok {
|
|
if retained.RetryCleanup() != nil {
|
|
return retained
|
|
}
|
|
return nil
|
|
}
|
|
|
|
retriers := cleanupRetriers(err)
|
|
if len(retriers) == 0 {
|
|
return nil
|
|
}
|
|
if len(retriers) == 1 {
|
|
if retriers[0].RetryCleanup() != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
retained := &retainedCleanupError{cause: err, retriers: retriers}
|
|
if retained.RetryCleanup() != nil {
|
|
return retained
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type retainedCleanupError struct {
|
|
cause error
|
|
retriers []cleanupRetrier
|
|
}
|
|
|
|
func (e *retainedCleanupError) Error() string { return e.cause.Error() }
|
|
func (e *retainedCleanupError) Unwrap() error { return e.cause }
|
|
|
|
func (e *retainedCleanupError) RetryCleanup() error {
|
|
remaining := e.retriers[:0]
|
|
var retryErr error
|
|
for _, retrier := range e.retriers {
|
|
if err := retrier.RetryCleanup(); err != nil {
|
|
remaining = append(remaining, retrier)
|
|
retryErr = errors.Join(retryErr, err)
|
|
}
|
|
}
|
|
e.retriers = remaining
|
|
return retryErr
|
|
}
|
|
|
|
func cleanupRetriers(err error) []cleanupRetrier {
|
|
var retriers []cleanupRetrier
|
|
stack := []error{err}
|
|
for len(stack) > 0 {
|
|
current := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
if current == nil {
|
|
continue
|
|
}
|
|
if retrier, ok := current.(cleanupRetrier); ok &&
|
|
!containsCleanupRetrier(retriers, retrier) {
|
|
retriers = append(retriers, retrier)
|
|
}
|
|
switch wrapped := current.(type) {
|
|
case interface{ Unwrap() []error }:
|
|
stack = append(stack, wrapped.Unwrap()...)
|
|
case interface{ Unwrap() error }:
|
|
stack = append(stack, wrapped.Unwrap())
|
|
}
|
|
}
|
|
return retriers
|
|
}
|
|
|
|
func containsCleanupRetrier(
|
|
retriers []cleanupRetrier, candidate cleanupRetrier,
|
|
) bool {
|
|
for _, retrier := range retriers {
|
|
left := reflect.ValueOf(retrier)
|
|
right := reflect.ValueOf(candidate)
|
|
if left.Type() != right.Type() {
|
|
continue
|
|
}
|
|
if left.Type().Comparable() && left.Interface() == right.Interface() {
|
|
return true
|
|
}
|
|
switch left.Kind() {
|
|
case reflect.Chan, reflect.Func, reflect.Pointer,
|
|
reflect.Slice, reflect.UnsafePointer:
|
|
if left.Pointer() == right.Pointer() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|