vectra-client 0.1.2 → 0.2.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.
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Instrumentation and observability hooks
5
+ #
6
+ # Provides hooks for monitoring tools like New Relic, Datadog, and custom loggers.
7
+ # Records metrics for all vector operations including duration, result counts, and errors.
8
+ #
9
+ # @example Enable instrumentation
10
+ # Vectra.configure do |config|
11
+ # config.instrumentation = true
12
+ # end
13
+ #
14
+ # @example Custom instrumentation handler
15
+ # Vectra.on_operation do |event|
16
+ # puts "#{event.operation} took #{event.duration}ms"
17
+ # end
18
+ #
19
+ module Instrumentation
20
+ # Event object passed to instrumentation handlers
21
+ #
22
+ # @attr_reader [Symbol] operation The operation type (:upsert, :query, :fetch, etc.)
23
+ # @attr_reader [Symbol] provider The provider name (:pinecone, :pgvector, etc.)
24
+ # @attr_reader [String] index The index/table name
25
+ # @attr_reader [Float] duration Duration in milliseconds
26
+ # @attr_reader [Hash] metadata Additional operation metadata
27
+ # @attr_reader [Exception, nil] error Exception if operation failed
28
+ class Event
29
+ attr_reader :operation, :provider, :index, :duration, :metadata, :error
30
+
31
+ def initialize(operation:, provider:, index:, duration:, metadata: {}, error: nil)
32
+ @operation = operation
33
+ @provider = provider
34
+ @index = index
35
+ @duration = duration
36
+ @metadata = metadata
37
+ @error = error
38
+ end
39
+
40
+ # Check if operation succeeded
41
+ #
42
+ # @return [Boolean]
43
+ def success?
44
+ error.nil?
45
+ end
46
+
47
+ # Check if operation failed
48
+ #
49
+ # @return [Boolean]
50
+ def failure?
51
+ !success?
52
+ end
53
+ end
54
+
55
+ class << self
56
+ # Register an instrumentation handler
57
+ #
58
+ # The block will be called for every vector operation with an Event object.
59
+ #
60
+ # @yield [event] The instrumentation event
61
+ # @yieldparam event [Event] Event details
62
+ #
63
+ # @example
64
+ # Vectra::Instrumentation.on_operation do |event|
65
+ # StatsD.timing("vectra.#{event.operation}", event.duration)
66
+ # StatsD.increment("vectra.#{event.operation}.#{event.success? ? 'success' : 'error'}")
67
+ # end
68
+ #
69
+ def on_operation(&block)
70
+ handlers << block
71
+ end
72
+
73
+ # Instrument a vector operation
74
+ #
75
+ # @param operation [Symbol] Operation name
76
+ # @param provider [Symbol] Provider name
77
+ # @param index [String] Index name
78
+ # @param metadata [Hash] Additional metadata
79
+ # @yield The operation to instrument
80
+ # @return [Object] The result of the block
81
+ #
82
+ # @api private
83
+ def instrument(operation:, provider:, index:, metadata: {})
84
+ return yield unless enabled?
85
+
86
+ start_time = Time.now
87
+ error = nil
88
+ result = nil
89
+
90
+ begin
91
+ result = yield
92
+ rescue StandardError => e
93
+ error = e
94
+ raise
95
+ ensure
96
+ duration = ((Time.now - start_time) * 1000).round(2)
97
+
98
+ event = Event.new(
99
+ operation: operation,
100
+ provider: provider,
101
+ index: index,
102
+ duration: duration,
103
+ metadata: metadata,
104
+ error: error
105
+ )
106
+
107
+ notify_handlers(event)
108
+ end
109
+
110
+ result
111
+ end
112
+
113
+ # Check if instrumentation is enabled
114
+ #
115
+ # @return [Boolean]
116
+ def enabled?
117
+ Vectra.configuration.instrumentation
118
+ end
119
+
120
+ # Clear all handlers (useful for testing)
121
+ #
122
+ # @api private
123
+ def clear_handlers!
124
+ @handlers = []
125
+ end
126
+
127
+ private
128
+
129
+ def handlers
130
+ @handlers ||= []
131
+ end
132
+
133
+ def notify_handlers(event)
134
+ handlers.each do |handler|
135
+ handler.call(event)
136
+ rescue StandardError => e
137
+ # Don't let instrumentation errors crash the app
138
+ warn "Vectra instrumentation handler error: #{e.message}"
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -5,6 +5,8 @@ module Vectra
5
5
  class Pgvector < Base
6
6
  # Connection management for pgvector provider
7
7
  module Connection
8
+ include Vectra::Retry
9
+
8
10
  private
9
11
 
10
12
  # Get or create database connection
@@ -49,7 +51,9 @@ module Vectra
49
51
  # Execute SQL with parameters
50
52
  def execute(sql, params = [])
51
53
  log_debug("Executing SQL", { sql: sql, params: params })
52
- connection.exec_params(sql, params)
54
+ with_retry do
55
+ connection.exec_params(sql, params)
56
+ end
53
57
  rescue PG::Error => e
54
58
  handle_pg_error(e)
55
59
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "connection_pool"
4
+
5
+ module Vectra
6
+ # Retry helper for transient errors
7
+ #
8
+ # Provides exponential backoff retry logic for database operations.
9
+ #
10
+ # @example
11
+ # include Vectra::Retry
12
+ #
13
+ # with_retry(max_attempts: 3) do
14
+ # connection.exec_params(sql, params)
15
+ # end
16
+ #
17
+ module Retry
18
+ # Errors that should be retried
19
+ RETRYABLE_PG_ERRORS = [
20
+ "PG::ConnectionBad",
21
+ "PG::UnableToSend",
22
+ "PG::AdminShutdown",
23
+ "PG::CrashShutdown",
24
+ "PG::CannotConnectNow",
25
+ "PG::TooManyConnections",
26
+ "PG::SerializationFailure",
27
+ "PG::DeadlockDetected"
28
+ ].freeze
29
+
30
+ # Execute block with retry logic
31
+ #
32
+ # @param max_attempts [Integer] Maximum number of attempts (default: from config)
33
+ # @param base_delay [Float] Initial delay in seconds (default: from config)
34
+ # @param max_delay [Float] Maximum delay in seconds (default: 30)
35
+ # @param backoff_factor [Float] Multiplier for each retry (default: 2)
36
+ # @param jitter [Boolean] Add randomness to delay (default: true)
37
+ # @yield The block to execute with retry logic
38
+ # @return [Object] The result of the block
39
+ #
40
+ # @example
41
+ # result = with_retry(max_attempts: 5) do
42
+ # perform_database_operation
43
+ # end
44
+ #
45
+ def with_retry(max_attempts: nil, base_delay: nil, max_delay: 30, backoff_factor: 2, jitter: true, &block)
46
+ max_attempts ||= config.max_retries
47
+ base_delay ||= config.retry_delay
48
+
49
+ attempt = 0
50
+ last_error = nil
51
+
52
+ loop do
53
+ attempt += 1
54
+
55
+ begin
56
+ return block.call
57
+ rescue StandardError => e
58
+ last_error = e
59
+
60
+ # Don't retry if not retryable or out of attempts
61
+ should_retry = retryable_error?(e) && attempt < max_attempts
62
+
63
+ unless should_retry
64
+ log_error("Operation failed after #{attempt} attempts", e)
65
+ raise
66
+ end
67
+
68
+ # Calculate delay with exponential backoff
69
+ delay = calculate_delay(
70
+ attempt: attempt,
71
+ base_delay: base_delay,
72
+ max_delay: max_delay,
73
+ backoff_factor: backoff_factor,
74
+ jitter: jitter
75
+ )
76
+
77
+ log_retry(attempt, max_attempts, delay, e)
78
+ sleep(delay)
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Check if error should be retried
86
+ #
87
+ # @param error [Exception] The error to check
88
+ # @return [Boolean]
89
+ def retryable_error?(error)
90
+ error_class = error.class.name
91
+
92
+ # Check if it's a retryable PG error
93
+ return true if RETRYABLE_PG_ERRORS.include?(error_class)
94
+
95
+ # Check if it's a connection pool timeout
96
+ return true if error.instance_of?(::ConnectionPool::TimeoutError)
97
+
98
+ # Check message for specific patterns
99
+ error_message = error.message.downcase
100
+ error_message.include?("timeout") ||
101
+ error_message.include?("connection") ||
102
+ error_message.include?("temporary")
103
+ end
104
+
105
+ # Calculate exponential backoff delay
106
+ #
107
+ # @param attempt [Integer] Current attempt number
108
+ # @param base_delay [Float] Base delay in seconds
109
+ # @param max_delay [Float] Maximum delay in seconds
110
+ # @param backoff_factor [Float] Multiplier for each retry
111
+ # @param jitter [Boolean] Add randomness
112
+ # @return [Float] Delay in seconds
113
+ def calculate_delay(attempt:, base_delay:, max_delay:, backoff_factor:, jitter:)
114
+ # Exponential backoff: base_delay * (backoff_factor ^ (attempt - 1))
115
+ delay = base_delay * (backoff_factor**(attempt - 1))
116
+
117
+ # Cap at max_delay
118
+ delay = [delay, max_delay].min
119
+
120
+ # Add jitter (±25% randomness)
121
+ if jitter
122
+ jitter_amount = delay * 0.25
123
+ delay += rand(-jitter_amount..jitter_amount)
124
+ end
125
+
126
+ delay.clamp(base_delay, max_delay)
127
+ end
128
+
129
+ # Log retry attempt
130
+ #
131
+ # @param attempt [Integer] Current attempt
132
+ # @param max_attempts [Integer] Maximum attempts
133
+ # @param delay [Float] Delay before next attempt
134
+ # @param error [Exception] The error that triggered retry
135
+ def log_retry(attempt, max_attempts, delay, error)
136
+ return unless config.logger
137
+
138
+ config.logger.warn(
139
+ "[Vectra] Retry attempt #{attempt}/#{max_attempts} after error: " \
140
+ "#{error.class} - #{error.message}. Waiting #{delay.round(2)}s..."
141
+ )
142
+ end
143
+
144
+ # Log final error
145
+ #
146
+ # @param message [String] Error message
147
+ # @param error [Exception] The error
148
+ def log_error(message, error)
149
+ return unless config.logger
150
+
151
+ config.logger.error(
152
+ "[Vectra] #{message}: #{error.class} - #{error.message}"
153
+ )
154
+ end
155
+ end
156
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vectra
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/vectra.rb CHANGED
@@ -5,6 +5,9 @@ require_relative "vectra/errors"
5
5
  require_relative "vectra/configuration"
6
6
  require_relative "vectra/vector"
7
7
  require_relative "vectra/query_result"
8
+ require_relative "vectra/instrumentation"
9
+ require_relative "vectra/retry"
10
+ require_relative "vectra/active_record"
8
11
  require_relative "vectra/providers/base"
9
12
  require_relative "vectra/providers/pinecone"
10
13
  require_relative "vectra/providers/qdrant"
@@ -53,6 +56,14 @@ require_relative "vectra/client"
53
56
  #
54
57
  module Vectra
55
58
  class << self
59
+ # Register an instrumentation handler
60
+ #
61
+ # @yield [event] The instrumentation event
62
+ # @see Instrumentation.on_operation
63
+ def on_operation(&)
64
+ Instrumentation.on_operation(&)
65
+ end
66
+
56
67
  # Create a new client with the given options
57
68
  #
58
69
  # @param options [Hash] client options
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: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mijo Kristo
@@ -37,6 +37,34 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activerecord
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: sqlite3
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.4'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.4'
40
68
  - !ruby/object:Gem::Dependency
41
69
  name: pg
42
70
  requirement: !ruby/object:Gem::Requirement
@@ -179,14 +207,29 @@ files:
179
207
  - CHANGELOG.md
180
208
  - CODE_OF_CONDUCT.md
181
209
  - CONTRIBUTING.md
210
+ - IMPLEMENTATION_GUIDE.md
182
211
  - LICENSE
212
+ - NEW_FEATURES_v0.2.0.md
183
213
  - README.md
214
+ - RELEASE_CHECKLIST_v0.2.0.md
184
215
  - Rakefile
185
216
  - SECURITY.md
217
+ - USAGE_EXAMPLES.md
218
+ - benchmarks/batch_operations_benchmark.rb
219
+ - benchmarks/connection_pooling_benchmark.rb
220
+ - examples/active_record_demo.rb
221
+ - examples/instrumentation_demo.rb
222
+ - lib/generators/vectra/install_generator.rb
223
+ - lib/generators/vectra/templates/enable_pgvector_extension.rb
224
+ - lib/generators/vectra/templates/vectra.rb
186
225
  - lib/vectra.rb
226
+ - lib/vectra/active_record.rb
187
227
  - lib/vectra/client.rb
188
228
  - lib/vectra/configuration.rb
189
229
  - lib/vectra/errors.rb
230
+ - lib/vectra/instrumentation.rb
231
+ - lib/vectra/instrumentation/datadog.rb
232
+ - lib/vectra/instrumentation/new_relic.rb
190
233
  - lib/vectra/providers/base.rb
191
234
  - lib/vectra/providers/pgvector.rb
192
235
  - lib/vectra/providers/pgvector/connection.rb
@@ -196,6 +239,7 @@ files:
196
239
  - lib/vectra/providers/qdrant.rb
197
240
  - lib/vectra/providers/weaviate.rb
198
241
  - lib/vectra/query_result.rb
242
+ - lib/vectra/retry.rb
199
243
  - lib/vectra/vector.rb
200
244
  - lib/vectra/version.rb
201
245
  homepage: https://github.com/stokry/vectra