Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
32 changes: 29 additions & 3 deletions contractcourt/channel_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2628,6 +2628,10 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) {
log.Tracef("ChannelArbitrator(%v): attempting to resolve %T",
c.cfg.ChanPoint, currentContract)

// retryAttempt tracks consecutive transient Resolve() errors for
// exponential backoff. Reset to zero on any successful Resolve().
retryAttempt := 0

// Until the contract is fully resolved, we'll continue to iteratively
// resolve the contract one step at a time.
for !currentContract.IsResolved() {
Expand All @@ -2649,12 +2653,34 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) {
return
}

// Instead of exiting the goroutine permanently
// on transient errors, retry with exponential
// backoff. This prevents silent resolver death
// from transient failures like brief bitcoind
// restarts or ZMQ disconnects, which could
// leave HTLC outputs completely unwatched.
log.Errorf("ChannelArbitrator(%v): unable to "+
"progress %T: %v",
c.cfg.ChanPoint, currentContract, err)
return
"progress %T: %v (retry in %v, "+
"attempt %d)",
c.cfg.ChanPoint, currentContract, err,
resolverRetryBackoff(retryAttempt),
retryAttempt+1)

select {
case <-time.After(
resolverRetryBackoff(retryAttempt),
):
case <-c.quit:
return
}

retryAttempt++
continue
}

// Resolve succeeded, reset retry counter.
retryAttempt = 0

switch {
// If this contract produced another, then this means
// the current contract was only able to be partially
Expand Down
21 changes: 21 additions & 0 deletions contractcourt/contract_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"sync/atomic"
"time"

"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
Expand Down Expand Up @@ -182,4 +183,24 @@ var (
// errResolverShuttingDown is returned when the resolver stops
// progressing because it received the quit signal.
errResolverShuttingDown = errors.New("resolver shutting down")

// resolverRetryInitBackoff is the initial backoff duration before
// retrying a failed Resolve() call.
resolverRetryInitBackoff = 5 * time.Second

// resolverRetryMaxBackoff is the maximum backoff duration for
// resolver retry. Capped to avoid excessive delays on persistent
// failures that require operator intervention.
resolverRetryMaxBackoff = 5 * time.Minute
)

// resolverRetryBackoff returns the backoff duration for the given retry
// attempt. The backoff doubles each attempt, starting from
// resolverRetryInitBackoff and capping at resolverRetryMaxBackoff.
func resolverRetryBackoff(attempt int) time.Duration {
backoff := resolverRetryInitBackoff << uint(attempt)
if backoff > resolverRetryMaxBackoff {
return resolverRetryMaxBackoff
}
return backoff
}
Loading