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.
- checksums.yaml +4 -4
- data/README.md +16 -0
- data/lib/nextsql/client.rb +85 -9
- data/lib/nextsql/errors.rb +9 -1
- data/lib/nextsql/field_encryption.rb +823 -0
- data/lib/nextsql/pool.rb +215 -0
- data/lib/nextsql/protocol.rb +26 -5
- data/lib/nextsql/version.rb +1 -1
- data/lib/nextsql.rb +7 -0
- metadata +6 -4
data/lib/nextsql/pool.rb
ADDED
|
@@ -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
|
data/lib/nextsql/protocol.rb
CHANGED
|
@@ -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
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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)
|
data/lib/nextsql/version.rb
CHANGED
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.
|
|
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
|
|
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.
|
|
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: []
|