ruby_volt 0.0.5
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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +20 -0
- data/README.md +282 -0
- data/lib/ruby_volt.rb +33 -0
- data/lib/ruby_volt/base.rb +105 -0
- data/lib/ruby_volt/connection.rb +150 -0
- data/lib/ruby_volt/data_type.rb +50 -0
- data/lib/ruby_volt/data_type/app_data_type.rb +24 -0
- data/lib/ruby_volt/data_type/app_data_type/procedure_call_status_code.rb +14 -0
- data/lib/ruby_volt/data_type/app_data_type/wire_type_info.rb +22 -0
- data/lib/ruby_volt/data_type/basic.rb +36 -0
- data/lib/ruby_volt/data_type/basic/fixed_size.rb +26 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/decimal.rb +45 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/float.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/geography_point.rb +28 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type.rb +20 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/byte.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/integer.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/long.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/short.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_byte.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_integer.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_long.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_short.rb +9 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/null.rb +13 -0
- data/lib/ruby_volt/data_type/basic/fixed_size/timestamp.rb +24 -0
- data/lib/ruby_volt/data_type/basic/geography.rb +61 -0
- data/lib/ruby_volt/data_type/basic/string.rb +19 -0
- data/lib/ruby_volt/data_type/basic/varbinary.rb +34 -0
- data/lib/ruby_volt/data_type/complex.rb +11 -0
- data/lib/ruby_volt/data_type/complex/parameter.rb +20 -0
- data/lib/ruby_volt/data_type/complex/parameter_set.rb +19 -0
- data/lib/ruby_volt/data_type/complex/serializable_exception.rb +43 -0
- data/lib/ruby_volt/data_type/complex/volt_table.rb +96 -0
- data/lib/ruby_volt/data_type/compound.rb +8 -0
- data/lib/ruby_volt/data_type/compound/array.rb +42 -0
- data/lib/ruby_volt/data_type/extensions.rb +20 -0
- data/lib/ruby_volt/exceptions.rb +67 -0
- data/lib/ruby_volt/helper.rb +29 -0
- data/lib/ruby_volt/message.rb +29 -0
- data/lib/ruby_volt/message/invocation_request.rb +18 -0
- data/lib/ruby_volt/message/login_message.rb +17 -0
- data/lib/ruby_volt/meta.rb +6 -0
- data/lib/ruby_volt/meta/geography.rb +165 -0
- data/lib/ruby_volt/read_partial.rb +23 -0
- data/lib/ruby_volt/response.rb +24 -0
- data/lib/ruby_volt/response/invocation_response.rb +64 -0
- data/lib/ruby_volt/response/login_response.rb +27 -0
- data/lib/ruby_volt/version.rb +3 -0
- data/lib/ruby_volt/volt_table.rb +37 -0
- metadata +108 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'ruby_volt/message'
|
2
|
+
require 'ruby_volt/response'
|
3
|
+
|
4
|
+
module RubyVolt
|
5
|
+
class Connection
|
6
|
+
attr_reader :host, :port, :socket, :login_data
|
7
|
+
|
8
|
+
def initialize(base, host, port, username, password)
|
9
|
+
@host, @port = host, port
|
10
|
+
@mutex = Mutex.new # Semaphore
|
11
|
+
@opaque = Helper.uniq_bytes(8)
|
12
|
+
define_singleton_method :base do
|
13
|
+
base
|
14
|
+
end
|
15
|
+
define_singleton_method :login! do
|
16
|
+
request = LoginMessage.new(login_protocol, servicename, username, password)
|
17
|
+
response_msg = dialog!(request, connect_timeout)
|
18
|
+
response = LoginResponse.new(response_msg).unpack!
|
19
|
+
@login_data = response.data
|
20
|
+
if logged_in?
|
21
|
+
puts "=== VoltDB wired [#{host}:#{port}]: connection_id=#{login_data[:connection_id]}" if logged_in?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
establish!
|
25
|
+
end
|
26
|
+
|
27
|
+
def login_protocol
|
28
|
+
base.login_protocol
|
29
|
+
end
|
30
|
+
|
31
|
+
def procedure_protocol
|
32
|
+
base.procedure_protocol
|
33
|
+
end
|
34
|
+
|
35
|
+
def servicename
|
36
|
+
base.servicename
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect_timeout
|
40
|
+
base.connect_timeout
|
41
|
+
end
|
42
|
+
|
43
|
+
def procedure_timeout
|
44
|
+
base.procedure_timeout
|
45
|
+
end
|
46
|
+
|
47
|
+
def inspect
|
48
|
+
"#<#{self.class.name} [#{host}:#{port}]: server_host_id=#{login_data[:server_host_id]} connection_id=#{login_data[:connection_id]} login_protocol=#{login_protocol} procedure_protocol=#{procedure_protocol}>"
|
49
|
+
end
|
50
|
+
|
51
|
+
def establish!
|
52
|
+
close!
|
53
|
+
attempts = 0
|
54
|
+
begin
|
55
|
+
@socket = ::Socket.tcp(host, port, nil, nil, {:connect_timeout => connect_timeout})
|
56
|
+
rescue ::SystemCallError => e
|
57
|
+
warn "#{e.class}: #{e}. Reinitializing connection..." unless attempts > 0
|
58
|
+
attempts += 1
|
59
|
+
retry
|
60
|
+
else
|
61
|
+
login!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def socket_open?
|
66
|
+
socket && !socket.closed?
|
67
|
+
end
|
68
|
+
|
69
|
+
def close!
|
70
|
+
@login_data = nil
|
71
|
+
if socket
|
72
|
+
begin
|
73
|
+
socket.close
|
74
|
+
rescue
|
75
|
+
ensure
|
76
|
+
@socket = nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def require_login!
|
82
|
+
establish! unless logged_in?
|
83
|
+
end
|
84
|
+
|
85
|
+
def logged_in?
|
86
|
+
login_data && login_data[:auth_result] == 0
|
87
|
+
end
|
88
|
+
|
89
|
+
def unavailable?
|
90
|
+
!logged_in?||!socket_open?
|
91
|
+
end
|
92
|
+
|
93
|
+
def available?
|
94
|
+
!unavailable?
|
95
|
+
end
|
96
|
+
|
97
|
+
def call_procedure(procedure, client_data = @opaque, *parameters)
|
98
|
+
# The procedure invocation request contains the procedure to be called by name, and the serialized parameters to the procedure. The message also includes an opaque 8 byte client data value which will be returned with the response, and can be used by the client to correlate requests with responses.
|
99
|
+
@mutex.synchronize do
|
100
|
+
require_login!
|
101
|
+
request = InvocationRequest.new(procedure_protocol, procedure, client_data, *parameters)
|
102
|
+
dialog!(request, procedure_timeout) # Byte string
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def ping
|
107
|
+
call_procedure("@Ping")
|
108
|
+
end
|
109
|
+
|
110
|
+
def benchmark(cycle = 1000)
|
111
|
+
Helper.benchmark(cycle) {ping} # call @Ping - system stored procedure
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def dialog!(request, timeout = nil)
|
117
|
+
begin
|
118
|
+
socket.read(socket.nread) if socket_open? && socket.nread > 0 # Flush data waiting to be read
|
119
|
+
socket.send(request.msg, 0) # Starting dialog
|
120
|
+
io_select(:read, ReadTimeout, timeout) do # Waiting socket to be readable
|
121
|
+
length = DataType::Integer.unpack(socket) # Unpacking 32-bit integer of message length
|
122
|
+
readmsg(length) # Receiving message body
|
123
|
+
end
|
124
|
+
rescue ::SystemCallError, SocketDialogTimeout, ConnectionLostStatusCode => e
|
125
|
+
warn "#{e.class}: #{e}."
|
126
|
+
establish! unless request.is_a?(LoginMessage)
|
127
|
+
retry
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def readmsg(length)
|
132
|
+
msg = ::String.new(:capacity => length)
|
133
|
+
socket.read(length, msg) # Reading message body
|
134
|
+
ReadPartial.new(msg)
|
135
|
+
end
|
136
|
+
|
137
|
+
def io_select(action, exception_class, timeout = DEFAULT_TIMEOUT, &block)
|
138
|
+
ready = case action
|
139
|
+
when :read then IO.select([socket], nil, nil, timeout) # ready for read
|
140
|
+
when :send then IO.select(nil, [socket], nil, timeout) # ready for write
|
141
|
+
when :exception then IO.select(nil, nil, [socket], timeout) # ready for exceptions
|
142
|
+
end
|
143
|
+
# 1) IO.select takes a set of sockets and waits until it's possible to read or write with them (or if error happens). It returns sockets event happened with.
|
144
|
+
# 2) array contains sockets that are checked for events. In your case you specify only sockets for reading.
|
145
|
+
# 3) IO.select returns an array of arrays of sockets. Element 0 contains sockets you can read from, element 1 - sockets you can write to and element 2 - sockets with errors.
|
146
|
+
ready ? yield : raise(exception_class, "#{timeout} sec data #{action} timed out")
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class << self
|
4
|
+
def voltDataType(val) # Ruby data type => Wire::DataType conversion
|
5
|
+
case val
|
6
|
+
when ::Array then Array
|
7
|
+
when NilClass then Null # Byte::NULL_INDICATOR
|
8
|
+
when ::Integer then
|
9
|
+
case val.bit_length
|
10
|
+
when (0..7) then Byte
|
11
|
+
when (8..15) then Short
|
12
|
+
when (16..31) then Integer
|
13
|
+
else
|
14
|
+
Long if val.bit_length >= 32
|
15
|
+
end
|
16
|
+
when ::Float then Float
|
17
|
+
when ::String then
|
18
|
+
val.encoding.name == "UTF-8" ? String : Varbinary # See "sometext".encoding
|
19
|
+
when ::Time then Timestamp
|
20
|
+
when ::BigDecimal then Decimal
|
21
|
+
when Meta::Geography::Point then GeographyPoint
|
22
|
+
when Meta::Geography::Polygon then Geography
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def classifyDataType(val)
|
27
|
+
case val
|
28
|
+
when Class then val if val < DataType
|
29
|
+
when ::String, ::Symbol then
|
30
|
+
begin
|
31
|
+
DataType.const_get(val)
|
32
|
+
rescue
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def testpacking(*vals)
|
39
|
+
unpack(ReadPartial.new(pack(*vals)))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
require 'ruby_volt/data_type/extensions'
|
47
|
+
require 'ruby_volt/data_type/basic'
|
48
|
+
require 'ruby_volt/data_type/compound'
|
49
|
+
require 'ruby_volt/data_type/complex'
|
50
|
+
require 'ruby_volt/data_type/app_data_type'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class AppDataType < Byte
|
4
|
+
extend Extensions::CodeValuesHash
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def pack(val)
|
8
|
+
val = val.is_a?(::Integer) ? val : code_values[val]
|
9
|
+
super(val)
|
10
|
+
end
|
11
|
+
|
12
|
+
def unpack(bytes)
|
13
|
+
if unpacked = super(bytes)
|
14
|
+
codes[unpacked]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'ruby_volt/data_type/app_data_type/procedure_call_status_code'
|
24
|
+
require 'ruby_volt/data_type/app_data_type/wire_type_info'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class ProcedureCallStatusCode < AppDataType
|
4
|
+
hash_codes [1, SuccessStatusCode], # SUCCESS
|
5
|
+
[-1, UserAbortStatusCode], # USER_ABORT
|
6
|
+
[-2, GracefulFailureStatusCode], # GRACEFUL_FAILURE
|
7
|
+
[-3, UnexpectedFailureStatusCode], # UNEXPECTED_FAILURE
|
8
|
+
[-4, ConnectionLostStatusCode] # CONNECTION_LOST
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class WireTypeInfo < AppDataType
|
4
|
+
hash_codes [-99, Array], # ARRAY
|
5
|
+
[1, Null], # NULL
|
6
|
+
[3, Byte], # TINYINT
|
7
|
+
[4, Short], # SMALLINT
|
8
|
+
[5, Integer], # INTEGER
|
9
|
+
[6, Long], # BIGINT
|
10
|
+
[8, Float], # FLOAT
|
11
|
+
[9, String], # STRING
|
12
|
+
[11, Timestamp], # TIMESTAMP
|
13
|
+
[22, Decimal], # DECIMAL
|
14
|
+
[25, Varbinary], # VARBINARY
|
15
|
+
[26, GeographyPoint], # GEOGRAPHY_POINT
|
16
|
+
[27, Geography] # GEOGRAPHY
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class Basic < self
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def intToBytes(length, value)
|
7
|
+
bitlength = length*8
|
8
|
+
div = nil
|
9
|
+
x = [[32, 'N'], [16, 'n'], [8, 'C']].find {|a| (div = bitlength.divmod(a[0])) && div[0] != 0 && div[1] == 0}
|
10
|
+
b = (2**x[0])-1
|
11
|
+
([value & b] + (2..div[0]).map {|i| (value >> x[0]*(i-1)) & b}).reverse.pack(x[1]*div[0])
|
12
|
+
end
|
13
|
+
|
14
|
+
def bytesToInt(length, bytes) # bytes = array of 8-bit unsigned
|
15
|
+
bytes = bytes.unpack("C#{length}")
|
16
|
+
most_significant_bit = 1 << 7
|
17
|
+
negative = (bytes[0] & most_significant_bit) != 0
|
18
|
+
unscaled_value = -(bytes[0] & most_significant_bit) << length*8-8
|
19
|
+
# Clear the highest bit
|
20
|
+
# Unleash the powers of the butterfly
|
21
|
+
bytes[0] &= ~most_significant_bit
|
22
|
+
# Get the 2's complement
|
23
|
+
(0..length-1).each {|i| unscaled_value += bytes[i] << ((length-1 - i) * 8)}
|
24
|
+
unscaled_value * -1 if negative
|
25
|
+
unscaled_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'ruby_volt/data_type/basic/fixed_size'
|
34
|
+
require 'ruby_volt/data_type/basic/varbinary'
|
35
|
+
require 'ruby_volt/data_type/basic/string'
|
36
|
+
require 'ruby_volt/data_type/basic/geography'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class FixedSize < Basic
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def pack(val)
|
7
|
+
val ||= self::NULL_INDICATOR
|
8
|
+
[val].pack(self::DIRECTIVE)
|
9
|
+
end
|
10
|
+
|
11
|
+
def unpack(bytes)
|
12
|
+
unpacked = bytes.read(self::LENGTH).unpack1(self::DIRECTIVE)
|
13
|
+
unpacked unless unpacked == self::NULL_INDICATOR
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type'
|
22
|
+
require 'ruby_volt/data_type/basic/fixed_size/float'
|
23
|
+
require 'ruby_volt/data_type/basic/fixed_size/geography_point'
|
24
|
+
require 'ruby_volt/data_type/basic/fixed_size/timestamp' # Date
|
25
|
+
require 'ruby_volt/data_type/basic/fixed_size/decimal'
|
26
|
+
require 'ruby_volt/data_type/basic/fixed_size/null'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'bigdecimal/util'
|
3
|
+
|
4
|
+
module RubyVolt
|
5
|
+
class DataType
|
6
|
+
class Decimal < FixedSize
|
7
|
+
DIRECTIVE = 'C16'
|
8
|
+
NULL_INDICATOR = -170141183460469231731687303715884105728 # SQL NULL indicator for object type serializations
|
9
|
+
LENGTH = 16
|
10
|
+
PRECISION = 38
|
11
|
+
SCALE = 12
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def pack(val)
|
15
|
+
((val == self::NULL_INDICATOR) || val.nil?) ? intToBytes(self::LENGTH, self::NULL_INDICATOR) : serialize(val)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unpack(bytes)
|
19
|
+
if (unscaled = bytesToInt(self::LENGTH, bytes.read(self::LENGTH))) && (unscaled != self::NULL_INDICATOR)
|
20
|
+
unscaled = unscaled.to_s
|
21
|
+
scaled = unscaled.insert(unscaled.size - self::SCALE, ".")
|
22
|
+
BigDecimal(scaled)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialize(val)
|
27
|
+
num = case val
|
28
|
+
when BigDecimal then val
|
29
|
+
else val.to_d
|
30
|
+
end
|
31
|
+
sign, digits, base, exponent = *num.split
|
32
|
+
scale = digits.size - exponent
|
33
|
+
precision = digits.size
|
34
|
+
raise(::ArgumentError, "Scale of this decimal is #{scale} and the max is #{self::SCALE}") if scale > self::SCALE
|
35
|
+
rest = precision - scale
|
36
|
+
raise(::ArgumentError, "Precision to the left of the decimal point is #{rest} and the max is #{self::PRECISION-self::SCALE}") if rest > 26
|
37
|
+
scale_factor = self::SCALE - scale
|
38
|
+
unscaled_int = sign * digits.to_i * base ** scale_factor # Unscaled integer
|
39
|
+
intToBytes(self::LENGTH, unscaled_int)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class GeographyPoint < FixedSize
|
4
|
+
DIRECTIVE = "G2"
|
5
|
+
LENGTH = 16
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def pack(point)
|
9
|
+
# A GeographyPointValue's wire protocol representation is simply two double precision numbers in sequence. The first is the longitude, and must be in the range -180 ≤ longitude ≤ 180. The second is the latitude, and must be in the range -90 ≤ latitude ≤ 90. The null GeographyPoint value has longitude and latitude both equal to 360.0.
|
10
|
+
if point.nil?
|
11
|
+
lng, lat = 360.0, 360.0
|
12
|
+
else
|
13
|
+
lng, lat = point.lng, point.lat
|
14
|
+
end
|
15
|
+
[lng, lat].pack(self::DIRECTIVE)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unpack(bytes)
|
19
|
+
lng, lat = *bytes.read(self::LENGTH).unpack(self::DIRECTIVE)
|
20
|
+
if (lng != 360.0) && (lat != 360.0)
|
21
|
+
Meta::Geography::Point.new(lng, lat)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RubyVolt
|
2
|
+
class DataType
|
3
|
+
class IntegerType < FixedSize
|
4
|
+
# Reserved class
|
5
|
+
|
6
|
+
class << self
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/byte'
|
14
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/u_byte'
|
15
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/short'
|
16
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/u_short'
|
17
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/integer'
|
18
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/u_integer'
|
19
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/long'
|
20
|
+
require 'ruby_volt/data_type/basic/fixed_size/integer_type/u_long'
|