Class: Woods::Resilience::RetryableProvider

Inherits:
Object
  • Object
show all
Includes:
Embedding::Provider::Interface
Defined in:
lib/woods/resilience/retryable_provider.rb

Overview

Wraps an embedding provider with retry logic and optional circuit breaker.

Transparently retries transient failures with exponential backoff. When a circuit breaker is provided, all calls are routed through it, and CircuitOpenError is never retried.

Examples:

Without circuit breaker

retryable = RetryableProvider.new(provider: ollama_provider, max_retries: 3)
vector = retryable.embed("some text")

With circuit breaker

breaker = CircuitBreaker.new(threshold: 5, reset_timeout: 60)
retryable = RetryableProvider.new(
  provider: ollama_provider,
  max_retries: 3,
  circuit_breaker: breaker
)
vector = retryable.embed("some text")

Constant Summary collapse

MAX_BACKOFF_SECONDS =

Maximum backoff delay in seconds. Without a cap, attempts 8+ sleep longer than most service-level timeouts (>25s) and compound retry storms across correlated workers.

30.0
BACKOFF_BASE =

Base multiplier for exponential backoff. Delay is roughly ‘BACKOFF_BASE * 2**attempt` with full jitter applied on top.

0.1

Instance Method Summary collapse

Constructor Details

#initialize(provider:, max_retries: 3, circuit_breaker: nil) ⇒ RetryableProvider

Returns a new instance of RetryableProvider.

Parameters:

  • provider (#embed, #embed_batch, #dimensions, #model_name)

    The underlying embedding provider

  • max_retries (Integer) (defaults to: 3)

    Maximum number of retry attempts

  • circuit_breaker (CircuitBreaker, nil) (defaults to: nil)

    Optional circuit breaker instance



32
33
34
35
36
# File 'lib/woods/resilience/retryable_provider.rb', line 32

def initialize(provider:, max_retries: 3, circuit_breaker: nil)
  @provider = provider
  @max_retries = max_retries
  @circuit_breaker = circuit_breaker
end

Instance Method Details

#dimensionsInteger

Return the dimensionality of the embedding vectors.

Returns:

  • (Integer)

    number of dimensions



61
62
63
# File 'lib/woods/resilience/retryable_provider.rb', line 61

def dimensions
  @provider.dimensions
end

#embed(text) ⇒ Array<Float>

Embed a single text string with retry logic.

Parameters:

  • text (String)

    the text to embed

Returns:

  • (Array<Float>)

    the embedding vector

Raises:

  • (CircuitOpenError)

    if the circuit breaker is open

  • (StandardError)

    if all retries are exhausted



44
45
46
# File 'lib/woods/resilience/retryable_provider.rb', line 44

def embed(text)
  with_retries { call_provider { @provider.embed(text) } }
end

#embed_batch(texts) ⇒ Array<Array<Float>>

Embed multiple texts with retry logic.

Parameters:

  • texts (Array<String>)

    the texts to embed

Returns:

  • (Array<Array<Float>>)

    array of embedding vectors

Raises:

  • (CircuitOpenError)

    if the circuit breaker is open

  • (StandardError)

    if all retries are exhausted



54
55
56
# File 'lib/woods/resilience/retryable_provider.rb', line 54

def embed_batch(texts)
  with_retries { call_provider { @provider.embed_batch(texts) } }
end

#max_input_tokensInteger?

Delegate the per-provider input cap. The retry wrapper does not change the provider’s budget, so just hand through whatever the inner provider reports. Without this, ‘respond_to?` returns true via Interface but the call raises NotImplementedError.

Returns:

  • (Integer, nil)


78
79
80
81
82
# File 'lib/woods/resilience/retryable_provider.rb', line 78

def max_input_tokens
  return @provider.max_input_tokens if @provider.respond_to?(:max_input_tokens)

  nil
end

#model_nameString

Return the name of the embedding model.

Returns:

  • (String)

    model name



68
69
70
# File 'lib/woods/resilience/retryable_provider.rb', line 68

def model_name
  @provider.model_name
end