bzync-nextsql 0.1.0 → 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,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module NextSQL
6
+ # Thread-safe connection pool for NextSQL Ruby driver.
7
+ #
8
+ # pool = NextSQL.create_pool(cfg, min_size: 2, max_size: 10)
9
+ # pool.with do |conn|
10
+ # res = conn.exec("SELECT * FROM users")
11
+ # end
12
+ #
13
+ # # Or directly on the pool:
14
+ # pool.exec("INSERT INTO logs VALUES ($1)", ["event"])
15
+ # pool.close
16
+ class ConnectionPool
17
+ class PooledItem
18
+ attr_reader :conn
19
+ attr_accessor :last_used
20
+
21
+ def initialize(conn)
22
+ @conn = conn
23
+ @last_used = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ end
25
+ end
26
+
27
+ def initialize(config, min_size: 1, max_size: 10, timeout: 30.0, max_idle: 300.0, statement_cache_size: 50)
28
+ raise Error.new("invalid_argument", "min_size must be >= 0") if min_size < 0
29
+ raise Error.new("invalid_argument", "max_size must be >= 1") if max_size < 1
30
+ raise Error.new("invalid_argument", "max_size must be >= min_size") if max_size < min_size
31
+ raise Error.new("invalid_argument", "statement_cache_size must be >= 0") if statement_cache_size < 0
32
+
33
+ @config = config.dup
34
+ @config.statement_cache_size = statement_cache_size if @config.respond_to?(:statement_cache_size=)
35
+ @min_size = min_size
36
+ @max_size = max_size
37
+ @timeout = timeout
38
+ @max_idle = max_idle
39
+
40
+ @monitor = Monitor.new
41
+ @cond = @monitor.new_cond
42
+ @idle = []
43
+ @in_use = {}.compare_by_identity
44
+ @closed = false
45
+
46
+ if min_size > 0
47
+ min_size.times do
48
+ begin
49
+ conn = Connection.connect(@config)
50
+ @idle << PooledItem.new(conn)
51
+ rescue StandardError
52
+ close
53
+ raise
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def size
60
+ @monitor.synchronize { @idle.size + @in_use.size }
61
+ end
62
+
63
+ def idle_count
64
+ @monitor.synchronize { @idle.size }
65
+ end
66
+
67
+ def in_use_count
68
+ @monitor.synchronize { @in_use.size }
69
+ end
70
+
71
+ def closed?
72
+ @monitor.synchronize { @closed }
73
+ end
74
+
75
+ # Acquires a connection from the pool, waiting up to +timeout+ seconds if
76
+ # all connections are currently in use.
77
+ def acquire(timeout: nil)
78
+ wait_time = timeout || @timeout
79
+ deadline = wait_time > 0 ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait_time : Float::INFINITY
80
+
81
+ @monitor.synchronize do
82
+ loop do
83
+ raise Error.new("unavailable", "connection pool is closed") if @closed
84
+
85
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
86
+ while (item = @idle.pop)
87
+ if @max_idle > 0 && (now - item.last_used) > @max_idle
88
+ safe_close(item.conn)
89
+ next
90
+ end
91
+ if !alive?(item.conn)
92
+ safe_close(item.conn)
93
+ next
94
+ end
95
+ @in_use[item.conn] = item
96
+ return item.conn
97
+ end
98
+
99
+ if (@idle.size + @in_use.size) < @max_size
100
+ conn = Connection.connect(@config)
101
+ item = PooledItem.new(conn)
102
+ @in_use[conn] = item
103
+ return conn
104
+ end
105
+
106
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
107
+ if remaining <= 0
108
+ raise Error.new("resource_exhausted", "connection pool exhausted, wait timeout exceeded")
109
+ end
110
+
111
+ @cond.wait([remaining, 0.5].min)
112
+ end
113
+ end
114
+ end
115
+
116
+ # Returns a connection to the pool.
117
+ def release(conn)
118
+ @monitor.synchronize do
119
+ item = @in_use.delete(conn)
120
+ return if item.nil?
121
+
122
+ if @closed
123
+ safe_close(conn)
124
+ @cond.signal
125
+ return
126
+ end
127
+
128
+ unless clean_connection(item)
129
+ safe_close(conn)
130
+ @cond.signal
131
+ return
132
+ end
133
+
134
+ item.last_used = Process.clock_gettime(Process::CLOCK_MONOTONIC)
135
+ @idle << item
136
+ @cond.signal
137
+ end
138
+ end
139
+
140
+ # Checks out a connection for the duration of the block and guarantees its
141
+ # release even if the block raises.
142
+ def with(timeout: nil)
143
+ conn = acquire(timeout: timeout)
144
+ begin
145
+ yield conn
146
+ ensure
147
+ release(conn)
148
+ end
149
+ end
150
+
151
+ # Executes a statement on a pooled connection and returns the result.
152
+ def exec(sql, params = nil)
153
+ with { |conn| conn.exec(sql, params) }
154
+ end
155
+
156
+ # Queries a statement on a pooled connection, buffering all rows into
157
+ # a Result before releasing the connection.
158
+ def query(sql, params = nil)
159
+ with do |conn|
160
+ rows = conn.query(sql, params)
161
+ rows.collect
162
+ end
163
+ end
164
+
165
+ # Closes all idle and active connections in the pool.
166
+ def close
167
+ @monitor.synchronize do
168
+ return if @closed
169
+
170
+ @closed = true
171
+ @idle.each { |item| safe_close(item.conn) }
172
+ @idle.clear
173
+
174
+ @in_use.each_key { |conn| safe_close(conn) }
175
+ @in_use.clear
176
+
177
+ @cond.broadcast
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def safe_close(conn)
184
+ conn.close
185
+ rescue StandardError
186
+ nil
187
+ end
188
+
189
+ def alive?(conn)
190
+ if conn.instance_variable_defined?(:@sock) && conn.instance_variable_get(:@sock).nil?
191
+ return false
192
+ end
193
+ true
194
+ rescue StandardError
195
+ true
196
+ end
197
+
198
+ def clean_connection(item)
199
+ item.conn.exec("ROLLBACK")
200
+ true
201
+ rescue StandardError
202
+ false
203
+ end
204
+ end
205
+
206
+ def self.create_pool(config, min_size: 1, max_size: 10, timeout: 30.0, max_idle: 300.0)
207
+ ConnectionPool.new(
208
+ config,
209
+ min_size: min_size,
210
+ max_size: max_size,
211
+ timeout: timeout,
212
+ max_idle: max_idle,
213
+ )
214
+ end
215
+ end
@@ -53,6 +53,12 @@ module NextSQL
53
53
  AUTH_PASSWORD = 1
54
54
  AUTH_PASSWORD_KEY = 2
55
55
  FLAG_CANCEL = 1
56
+ # FLAG_PUBLIC_ERROR_CODES asks the server for the stable ERR_* error
57
+ # taxonomy (docs/error-codes.md). A server that does not implement it
58
+ # ignores the bit and keeps the NSQL v1 error shape, which decode_error
59
+ # still reads, so setting it is safe against any server version. The server
60
+ # echoes it in the hello-ok flags when it was accepted.
61
+ FLAG_PUBLIC_ERROR_CODES = 2
56
62
  FLAG_NULL = 0x01
57
63
 
58
64
  KIND_UUID = 1
@@ -313,9 +319,16 @@ module NextSQL
313
319
  end
314
320
 
315
321
  def decode_hello_ok(b)
316
- raise ProtocolError, "bad hello-ok length" unless b.bytesize == 11
317
-
318
- [u16(b, 0), b.getbyte(2), b.byteslice(3, 8)]
322
+ raise ProtocolError, "bad hello-ok length" unless [11, 13].include?(b.bytesize)
323
+
324
+ flags = 0
325
+ if b.bytesize == 13
326
+ flags = u16(b, 11)
327
+ # The server omits the field when it accepted no capability, so a
328
+ # present-but-zero field is a second encoding of the v1 hello-ok.
329
+ raise ProtocolError, "empty hello-ok flags" if flags.zero?
330
+ end
331
+ [u16(b, 0), b.getbyte(2), b.byteslice(3, 8), flags]
319
332
  end
320
333
 
321
334
  def encode_query(sql, params)
@@ -946,8 +959,16 @@ module NextSQL
946
959
 
947
960
  def decode_error(b)
948
961
  code, off = read_u16_string(b, 0, MAX_NAME)
949
- msg, = read_u16_string(b, off, MAX_NAME)
950
- Error.new(code, msg)
962
+ msg, off = read_u16_string(b, off, MAX_NAME)
963
+ # Optional trailing field, present only from a server that accepted
964
+ # FLAG_PUBLIC_ERROR_CODES. Its absence is normal -- an older server
965
+ # ignores the request bit -- so this must never be required.
966
+ public_code = ""
967
+ if off < b.bytesize
968
+ public_code, = read_u16_string(b, off, MAX_NAME)
969
+ raise ProtocolError, "empty public error code" if public_code.empty?
970
+ end
971
+ Error.new(code, msg, public_code)
951
972
  end
952
973
 
953
974
  def encode_set_read_consistency(mode, max_staleness_ms)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NextSQL
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/nextsql.rb CHANGED
@@ -25,8 +25,10 @@
25
25
  require_relative "nextsql/version"
26
26
  require_relative "nextsql/errors"
27
27
  require_relative "nextsql/protocol"
28
+ require_relative "nextsql/field_encryption"
28
29
  require_relative "nextsql/client"
29
30
  require_relative "nextsql/cluster"
31
+ require_relative "nextsql/pool"
30
32
 
31
33
  module NextSQL
32
34
  READ_STRONG = Protocol::READ_STRONG
@@ -65,4 +67,9 @@ module NextSQL
65
67
  def self.connect_cluster(cfg)
66
68
  Cluster.connect(cfg)
67
69
  end
70
+
71
+ # Creates a connection pool managing a set of connections to a NextSQL server.
72
+ def self.create_pool(cfg, **kwargs)
73
+ ConnectionPool.new(cfg, **kwargs)
74
+ end
68
75
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bzync-nextsql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rayan Reynaldo
@@ -10,8 +10,8 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: 'Native-protocol client for NextSQL: TLS 1.3, prepared statements, follower-read
13
- routing, and NSCE1 client-side field encryption. Keys and passwords are never accepted
14
- in a URL.'
13
+ routing, and NSCE1/NSCE2 client-side field encryption. Keys and passwords are never
14
+ accepted in a URL.'
15
15
  executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
@@ -22,6 +22,8 @@ files:
22
22
  - lib/nextsql/client.rb
23
23
  - lib/nextsql/cluster.rb
24
24
  - lib/nextsql/errors.rb
25
+ - lib/nextsql/field_encryption.rb
26
+ - lib/nextsql/pool.rb
25
27
  - lib/nextsql/protocol.rb
26
28
  - lib/nextsql/version.rb
27
29
  homepage: https://nextsql.bzync.com/docs/drivers
@@ -47,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
49
  - !ruby/object:Gem::Version
48
50
  version: '0'
49
51
  requirements: []
50
- rubygems_version: 4.0.20
52
+ rubygems_version: 4.0.23
51
53
  specification_version: 4
52
54
  summary: Official NextSQL Ruby driver. Speaks the native NSQL v1 protocol.
53
55
  test_files: []