Skip to content
skyl

Error.Retryable

Reports whether retrying could plausibly succeed.

Retryable is the predicate Client's retry loop uses. It is exported so you can make the same judgement at your own level.

Reference#

func (e *Error) Retryable() bool

Returns

true for rate limits, server errors, and unclassified failures that got no reply at all. false for everything else.

The rule#

switch {
case errors.Is(e.Kind, ErrRateLimit), errors.Is(e.Kind, ErrServer):
	return true
case e.Kind == nil:
	// Unclassified with no reply at all — a dial timeout, a reset
	// connection. Worth another attempt.
	return e.StatusCode == 0
default:
	return false
}
switch {
case errors.Is(e.Kind, ErrRateLimit), errors.Is(e.Kind, ErrServer):
	return true
case e.Kind == nil:
	// Unclassified with no reply at all — a dial timeout, a reset
	// connection. Worth another attempt.
	return e.StatusCode == 0
default:
	return false
}

Caveats

  • An unclassified error with a status is not retryable. The provider did answer; skyl just could not classify what it said, and repeating the request is unlikely to produce a more classifiable answer.
  • ErrAuth, ErrBadRequest, ErrNotFound, ErrRefusal and ErrUnsupported are never retryable — retrying burns quota to receive the same answer.
  • Certificate failures are not retried despite arriving as transport failures. A rejected certificate is a misconfiguration, not a blip.
  • Client.Models additionally refuses to retry ErrUnsupported, because a provider that cannot list models never will.

Usage#

Deciding whether to alert or back off

goCompiles
var e *skyl.Error
if errors.As(err, &e) {
	if e.Retryable() {
		metrics.Inc("skyl.transient", "provider", e.Provider)
	} else {
		// Will never succeed on its own.
		alerts.Page("skyl permanent failure", "provider", e.Provider, "kind", e.Kind)
	}
}
var e *skyl.Error
if errors.As(err, &e) {
	if e.Retryable() {
		metrics.Inc("skyl.transient", "provider", e.Provider)
	} else {
		// Will never succeed on its own.
		alerts.Page("skyl permanent failure", "provider", e.Provider, "kind", e.Kind)
	}
}

Retrying at your own level

goCompiles
// Safe in a batch job, where nothing has been displayed yet.
for attempt := 0; attempt < 3; attempt++ {
	resp, err := collect(ctx, client, req)
	if err == nil {
		return resp, nil
	}
	var e *skyl.Error
	if !errors.As(err, &e) || !e.Retryable() {
		return nil, err
	}
}
// Safe in a batch job, where nothing has been displayed yet.
for attempt := 0; attempt < 3; attempt++ {
	resp, err := collect(ctx, client, req)
	if err == nil {
		return resp, nil
	}
	var e *skyl.Error
	if !errors.As(err, &e) || !e.Retryable() {
		return nil, err
	}
}

Troubleshooting#

A certificate error reported Retryable() == false

Correct, and deliberate. Retrying an expired certificate spends the whole budget to receive the same answer, and delays the error the operator needs to see at exactly the moment they are debugging a connection problem.

Client retried something Retryable() says is false

It should not. If you saw that, the error reaching Client was a different value from the one you inspected — most likely an unclassified transport error with StatusCode == 0, which is retryable.

Edit this page on GitHub