vectra-client 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +53 -0
- data/docs/_layouts/home.html +1 -1
- data/docs/api/overview.md +6 -0
- data/docs/guides/middleware.md +324 -0
- data/examples/middleware_demo.rb +103 -0
- data/lib/vectra/client.rb +71 -15
- data/lib/vectra/health_check.rb +4 -2
- data/lib/vectra/middleware/base.rb +97 -0
- data/lib/vectra/middleware/cost_tracker.rb +121 -0
- data/lib/vectra/middleware/instrumentation.rb +44 -0
- data/lib/vectra/middleware/logging.rb +62 -0
- data/lib/vectra/middleware/pii_redaction.rb +65 -0
- data/lib/vectra/middleware/request.rb +62 -0
- data/lib/vectra/middleware/response.rb +65 -0
- data/lib/vectra/middleware/retry.rb +103 -0
- data/lib/vectra/middleware/stack.rb +74 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +9 -0
- metadata +12 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Response object returned through middleware chain
|
|
6
|
+
#
|
|
7
|
+
# @example Success response
|
|
8
|
+
# response = Response.new(result: { success: true })
|
|
9
|
+
# response.success? # => true
|
|
10
|
+
# response.result # => { success: true }
|
|
11
|
+
#
|
|
12
|
+
# @example Error response
|
|
13
|
+
# response = Response.new(error: StandardError.new('Failed'))
|
|
14
|
+
# response.failure? # => true
|
|
15
|
+
# response.error # => #<StandardError: Failed>
|
|
16
|
+
#
|
|
17
|
+
# @example With metadata
|
|
18
|
+
# response = Response.new(result: [])
|
|
19
|
+
# response.metadata[:duration_ms] = 45
|
|
20
|
+
# response.metadata[:cache_hit] = true
|
|
21
|
+
#
|
|
22
|
+
class Response
|
|
23
|
+
attr_accessor :result, :error, :metadata
|
|
24
|
+
|
|
25
|
+
# @param result [Object] The successful result
|
|
26
|
+
# @param error [Exception, nil] The error if failed
|
|
27
|
+
def initialize(result: nil, error: nil)
|
|
28
|
+
@result = result
|
|
29
|
+
@error = error
|
|
30
|
+
@metadata = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if the response was successful
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean] true if no error
|
|
36
|
+
def success?
|
|
37
|
+
error.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if the response failed
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean] true if error present
|
|
43
|
+
def failure?
|
|
44
|
+
!success?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raise error if present
|
|
48
|
+
#
|
|
49
|
+
# @raise [Exception] The stored error
|
|
50
|
+
# @return [void]
|
|
51
|
+
def raise_if_error!
|
|
52
|
+
raise error if error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get the result or raise error
|
|
56
|
+
#
|
|
57
|
+
# @return [Object] The result
|
|
58
|
+
# @raise [Exception] If error present
|
|
59
|
+
def value!
|
|
60
|
+
raise_if_error!
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Retry middleware for handling transient failures
|
|
6
|
+
#
|
|
7
|
+
# Automatically retries failed requests with configurable backoff strategy.
|
|
8
|
+
#
|
|
9
|
+
# @example With default settings (3 attempts, exponential backoff)
|
|
10
|
+
# Vectra::Client.use Vectra::Middleware::Retry
|
|
11
|
+
#
|
|
12
|
+
# @example With custom settings
|
|
13
|
+
# Vectra::Client.use Vectra::Middleware::Retry, max_attempts: 5, backoff: :linear
|
|
14
|
+
#
|
|
15
|
+
# @example Per-client retry
|
|
16
|
+
# client = Vectra::Client.new(
|
|
17
|
+
# provider: :pinecone,
|
|
18
|
+
# middleware: [[Vectra::Middleware::Retry, { max_attempts: 3 }]]
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
class Retry < Base
|
|
22
|
+
# @param max_attempts [Integer] Maximum number of attempts (default: 3)
|
|
23
|
+
# @param backoff [Symbol, Numeric] Backoff strategy (:exponential, :linear) or fixed delay
|
|
24
|
+
def initialize(max_attempts: 3, backoff: :exponential)
|
|
25
|
+
super()
|
|
26
|
+
@max_attempts = max_attempts
|
|
27
|
+
@backoff = backoff
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(request, app)
|
|
31
|
+
attempt = 0
|
|
32
|
+
last_error = nil
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
attempt += 1
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
response = app.call(request)
|
|
39
|
+
|
|
40
|
+
# If successful, return immediately
|
|
41
|
+
if response.success?
|
|
42
|
+
response.metadata[:retry_count] = attempt - 1
|
|
43
|
+
return response
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# If error is retryable and we haven't exceeded max attempts, retry
|
|
47
|
+
if response.error && retryable?(response.error) && attempt < @max_attempts
|
|
48
|
+
sleep(backoff_delay(attempt))
|
|
49
|
+
next
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Error is not retryable or max attempts reached, return response
|
|
53
|
+
response.metadata[:retry_count] = attempt - 1
|
|
54
|
+
return response
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
last_error = e
|
|
57
|
+
|
|
58
|
+
# If error is retryable and we haven't exceeded max attempts, retry
|
|
59
|
+
if retryable?(e) && attempt < @max_attempts
|
|
60
|
+
sleep(backoff_delay(attempt))
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Error is not retryable or max attempts reached, raise
|
|
65
|
+
raise
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Check if error is retryable
|
|
73
|
+
#
|
|
74
|
+
# @param error [Exception] The error to check
|
|
75
|
+
# @return [Boolean] true if error is retryable
|
|
76
|
+
def retryable?(error)
|
|
77
|
+
error.is_a?(Vectra::RateLimitError) ||
|
|
78
|
+
error.is_a?(Vectra::ConnectionError) ||
|
|
79
|
+
error.is_a?(Vectra::TimeoutError) ||
|
|
80
|
+
error.is_a?(Vectra::ServerError)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Calculate backoff delay
|
|
84
|
+
#
|
|
85
|
+
# @param attempt [Integer] Current attempt number
|
|
86
|
+
# @return [Float] Delay in seconds
|
|
87
|
+
def backoff_delay(attempt)
|
|
88
|
+
case @backoff
|
|
89
|
+
when :exponential
|
|
90
|
+
# 0.2s, 0.4s, 0.8s, 1.6s, ...
|
|
91
|
+
(2**(attempt - 1)) * 0.2
|
|
92
|
+
when :linear
|
|
93
|
+
# 0.5s, 1.0s, 1.5s, 2.0s, ...
|
|
94
|
+
attempt * 0.5
|
|
95
|
+
when Numeric
|
|
96
|
+
@backoff
|
|
97
|
+
else
|
|
98
|
+
1.0
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
module Middleware
|
|
5
|
+
# Middleware stack executor
|
|
6
|
+
#
|
|
7
|
+
# Builds and executes a chain of middleware around provider calls.
|
|
8
|
+
# Similar to Rack middleware, each middleware wraps the next one
|
|
9
|
+
# in the chain until reaching the actual provider.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# provider = Vectra::Providers::Memory.new
|
|
13
|
+
# middlewares = [LoggingMiddleware.new, RetryMiddleware.new]
|
|
14
|
+
# stack = Stack.new(provider, middlewares)
|
|
15
|
+
#
|
|
16
|
+
# result = stack.call(:upsert, index: 'test', vectors: [...])
|
|
17
|
+
#
|
|
18
|
+
class Stack
|
|
19
|
+
# @param provider [Vectra::Providers::Base] The actual provider
|
|
20
|
+
# @param middlewares [Array<Base>] Array of middleware instances
|
|
21
|
+
def initialize(provider, middlewares = [])
|
|
22
|
+
@provider = provider
|
|
23
|
+
@middlewares = middlewares
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute the middleware stack for an operation
|
|
27
|
+
#
|
|
28
|
+
# @param operation [Symbol] The operation to perform (:upsert, :query, etc.)
|
|
29
|
+
# @param params [Hash] The operation parameters
|
|
30
|
+
# @return [Object] The result from the provider
|
|
31
|
+
# @raise [Exception] Any error from middleware or provider
|
|
32
|
+
def call(operation, **params)
|
|
33
|
+
request = Request.new(operation: operation, **params)
|
|
34
|
+
|
|
35
|
+
# Build middleware chain
|
|
36
|
+
app = build_chain(request)
|
|
37
|
+
|
|
38
|
+
# Execute chain
|
|
39
|
+
response = app.call(request)
|
|
40
|
+
|
|
41
|
+
# Raise if error occurred
|
|
42
|
+
raise response.error if response.error
|
|
43
|
+
|
|
44
|
+
response.result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Build the middleware chain
|
|
50
|
+
#
|
|
51
|
+
# @param request [Request] The request object (unused here, but available)
|
|
52
|
+
# @return [Proc] The complete middleware chain
|
|
53
|
+
def build_chain(_request)
|
|
54
|
+
# Final app: actual provider call
|
|
55
|
+
final_app = lambda do |req|
|
|
56
|
+
# Remove middleware-specific params before calling provider
|
|
57
|
+
provider_params = req.to_h.except(:provider)
|
|
58
|
+
result = @provider.public_send(req.operation, **provider_params)
|
|
59
|
+
Response.new(result: result)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Response.new(error: e)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Wrap with middlewares in reverse order
|
|
65
|
+
# (last middleware in array is first to execute)
|
|
66
|
+
@middlewares.reverse.inject(final_app) do |next_app, middleware|
|
|
67
|
+
lambda do |req|
|
|
68
|
+
middleware.call(req, next_app)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/vectra/version.rb
CHANGED
data/lib/vectra.rb
CHANGED
|
@@ -18,6 +18,15 @@ require_relative "vectra/health_check"
|
|
|
18
18
|
require_relative "vectra/credential_rotation"
|
|
19
19
|
require_relative "vectra/audit_log"
|
|
20
20
|
require_relative "vectra/active_record"
|
|
21
|
+
require_relative "vectra/middleware/request"
|
|
22
|
+
require_relative "vectra/middleware/response"
|
|
23
|
+
require_relative "vectra/middleware/base"
|
|
24
|
+
require_relative "vectra/middleware/stack"
|
|
25
|
+
require_relative "vectra/middleware/logging"
|
|
26
|
+
require_relative "vectra/middleware/retry"
|
|
27
|
+
require_relative "vectra/middleware/instrumentation"
|
|
28
|
+
require_relative "vectra/middleware/pii_redaction"
|
|
29
|
+
require_relative "vectra/middleware/cost_tracker"
|
|
21
30
|
require_relative "vectra/providers/base"
|
|
22
31
|
require_relative "vectra/providers/pinecone"
|
|
23
32
|
require_relative "vectra/providers/qdrant"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vectra-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mijo Kristo
|
|
@@ -268,6 +268,7 @@ files:
|
|
|
268
268
|
- docs/guides/faq.md
|
|
269
269
|
- docs/guides/getting-started.md
|
|
270
270
|
- docs/guides/installation.md
|
|
271
|
+
- docs/guides/middleware.md
|
|
271
272
|
- docs/guides/monitoring.md
|
|
272
273
|
- docs/guides/performance.md
|
|
273
274
|
- docs/guides/rails-integration.md
|
|
@@ -294,6 +295,7 @@ files:
|
|
|
294
295
|
- examples/grafana-dashboard.json
|
|
295
296
|
- examples/grafana-setup.md
|
|
296
297
|
- examples/instrumentation_demo.rb
|
|
298
|
+
- examples/middleware_demo.rb
|
|
297
299
|
- examples/prometheus-exporter.rb
|
|
298
300
|
- lib/generators/vectra/index_generator.rb
|
|
299
301
|
- lib/generators/vectra/install_generator.rb
|
|
@@ -316,6 +318,15 @@ files:
|
|
|
316
318
|
- lib/vectra/instrumentation/new_relic.rb
|
|
317
319
|
- lib/vectra/instrumentation/sentry.rb
|
|
318
320
|
- lib/vectra/logging.rb
|
|
321
|
+
- lib/vectra/middleware/base.rb
|
|
322
|
+
- lib/vectra/middleware/cost_tracker.rb
|
|
323
|
+
- lib/vectra/middleware/instrumentation.rb
|
|
324
|
+
- lib/vectra/middleware/logging.rb
|
|
325
|
+
- lib/vectra/middleware/pii_redaction.rb
|
|
326
|
+
- lib/vectra/middleware/request.rb
|
|
327
|
+
- lib/vectra/middleware/response.rb
|
|
328
|
+
- lib/vectra/middleware/retry.rb
|
|
329
|
+
- lib/vectra/middleware/stack.rb
|
|
319
330
|
- lib/vectra/pool.rb
|
|
320
331
|
- lib/vectra/providers/base.rb
|
|
321
332
|
- lib/vectra/providers/memory.rb
|