1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
package logic

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// Polite polling (rss-kakapo).
//
// Upstream re-downloads every feed in full on every check, and retries a
// failing feed on its normal schedule forever. Here a check sends the
// validators of the last 200 response back (If-None-Match/If-Modified-Since),
// so an unchanged feed costs a 304 with no body; and every consecutive failure
// doubles the wait before the next attempt, while a server that says when to
// come back (Retry-After on a 429 or 503) is listened to.
//
// Neither may silence a feed for long, though: a misconfigured server can send
// "Retry-After: 3024000" (five weeks), and a feed that is down for a while
// would otherwise back off for days. So the wait after a failure is capped at
// max_retry_delay_hours (a day by default) — except for feeds whose normal
// interval is longer than that, which simply stay on their normal schedule,
// so that a failing feed is never polled more often than a healthy one.

const (
	// maxBackoffFailures caps the doubling: 2^6 = 64 times the normal interval.
	maxBackoffFailures = 6
	// defaultMaxRetryDelay is the cap when max_retry_delay_hours is not set.
	defaultMaxRetryDelay = 24 * time.Hour
)

// feedFetchError is a failed feed fetch. RetryAfter is what the server asked
// for (zero if it said nothing).
type feedFetchError struct {
	StatusCode int
	RetryAfter time.Duration
}

func (e *feedFetchError) Error() string {
	if e.RetryAfter > 0 {
		return fmt.Sprintf("request failed with status %d (retry after %s)", e.StatusCode, e.RetryAfter)
	}
	return fmt.Sprintf("request failed with status %d", e.StatusCode)
}

// parseRetryAfter reads a Retry-After header, which is either a number of
// seconds or an HTTP date. Unparseable or past values yield zero.
func parseRetryAfter(val string, now time.Time) time.Duration {
	val = strings.TrimSpace(val)
	if val == "" {
		return 0
	}
	if secs, err := strconv.Atoi(val); err == nil {
		if secs <= 0 {
			return 0
		}
		return time.Duration(secs) * time.Second
	}
	if t, err := http.ParseTime(val); err == nil {
		if d := t.Sub(now); d > 0 {
			return d
		}
	}
	return 0
}

// backoffDelay is how long to wait before checking a feed again after its
// failures-th consecutive failed check: the normal interval doubled once per
// failure, or longer if the server asked for more via Retry-After — but never
// longer than maxDelay, or than the normal interval if that is longer still.
func backoffDelay(interval time.Duration, failures int, retryAfter, maxDelay time.Duration) time.Duration {
	if failures < 1 {
		failures = 1
	}
	if failures > maxBackoffFailures {
		failures = maxBackoffFailures
	}
	delay := interval << failures
	if retryAfter > delay {
		delay = retryAfter
	}
	ceiling := maxDelay
	if interval > ceiling {
		ceiling = interval
	}
	if delay > ceiling {
		delay = ceiling
	}
	return delay
}

// maxRetryDelay is the configured cap on the wait after a failed check.
func maxRetryDelay(hours int) time.Duration {
	if hours <= 0 {
		return defaultMaxRetryDelay
	}
	return time.Duration(hours) * time.Hour
}