ruby_volt 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +20 -0
  4. data/README.md +282 -0
  5. data/lib/ruby_volt.rb +33 -0
  6. data/lib/ruby_volt/base.rb +105 -0
  7. data/lib/ruby_volt/connection.rb +150 -0
  8. data/lib/ruby_volt/data_type.rb +50 -0
  9. data/lib/ruby_volt/data_type/app_data_type.rb +24 -0
  10. data/lib/ruby_volt/data_type/app_data_type/procedure_call_status_code.rb +14 -0
  11. data/lib/ruby_volt/data_type/app_data_type/wire_type_info.rb +22 -0
  12. data/lib/ruby_volt/data_type/basic.rb +36 -0
  13. data/lib/ruby_volt/data_type/basic/fixed_size.rb +26 -0
  14. data/lib/ruby_volt/data_type/basic/fixed_size/decimal.rb +45 -0
  15. data/lib/ruby_volt/data_type/basic/fixed_size/float.rb +9 -0
  16. data/lib/ruby_volt/data_type/basic/fixed_size/geography_point.rb +28 -0
  17. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type.rb +20 -0
  18. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/byte.rb +9 -0
  19. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/integer.rb +9 -0
  20. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/long.rb +9 -0
  21. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/short.rb +9 -0
  22. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_byte.rb +9 -0
  23. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_integer.rb +9 -0
  24. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_long.rb +9 -0
  25. data/lib/ruby_volt/data_type/basic/fixed_size/integer_type/u_short.rb +9 -0
  26. data/lib/ruby_volt/data_type/basic/fixed_size/null.rb +13 -0
  27. data/lib/ruby_volt/data_type/basic/fixed_size/timestamp.rb +24 -0
  28. data/lib/ruby_volt/data_type/basic/geography.rb +61 -0
  29. data/lib/ruby_volt/data_type/basic/string.rb +19 -0
  30. data/lib/ruby_volt/data_type/basic/varbinary.rb +34 -0
  31. data/lib/ruby_volt/data_type/complex.rb +11 -0
  32. data/lib/ruby_volt/data_type/complex/parameter.rb +20 -0
  33. data/lib/ruby_volt/data_type/complex/parameter_set.rb +19 -0
  34. data/lib/ruby_volt/data_type/complex/serializable_exception.rb +43 -0
  35. data/lib/ruby_volt/data_type/complex/volt_table.rb +96 -0
  36. data/lib/ruby_volt/data_type/compound.rb +8 -0
  37. data/lib/ruby_volt/data_type/compound/array.rb +42 -0
  38. data/lib/ruby_volt/data_type/extensions.rb +20 -0
  39. data/lib/ruby_volt/exceptions.rb +67 -0
  40. data/lib/ruby_volt/helper.rb +29 -0
  41. data/lib/ruby_volt/message.rb +29 -0
  42. data/lib/ruby_volt/message/invocation_request.rb +18 -0
  43. data/lib/ruby_volt/message/login_message.rb +17 -0
  44. data/lib/ruby_volt/meta.rb +6 -0
  45. data/lib/ruby_volt/meta/geography.rb +165 -0
  46. data/lib/ruby_volt/read_partial.rb +23 -0
  47. data/lib/ruby_volt/response.rb +24 -0
  48. data/lib/ruby_volt/response/invocation_response.rb +64 -0
  49. data/lib/ruby_volt/response/login_response.rb +27 -0
  50. data/lib/ruby_volt/version.rb +3 -0
  51. data/lib/ruby_volt/volt_table.rb +37 -0
  52. metadata +108 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c0cc1d7254943ed092af2bc346a25c84af35eae4285ce97e400f2a50fba899b
4
+ data.tar.gz: a5357c99c85e51158e953679a45c56d340520ba3f7b72f060abb994f16842e08
5
+ SHA512:
6
+ metadata.gz: 2f177966f32af3efa1cd31638101185ac82d46d49a2bb62671c0435a513775b10d3ea5c3c232fc1a10601cbacad6a454dc1b1359224f2a837f45f90d2554f222
7
+ data.tar.gz: c0655ddb572c884fba6314907f4944973c0bf851709d46551b8387c3341ded8945ac5c3fdd815e9ca9531e1bcc90ce1248a26f036c23d9a05a28c7ad25102134
@@ -0,0 +1,3 @@
1
+ ## [0.0.5] - 2019-05-09
2
+ ### Initial release
3
+ - See docs and just try it!
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,282 @@
1
+ # RubyVolt
2
+
3
+ ### VoltDB Wire Protocol Client for Ruby programming language
4
+
5
+ #### Pure Ruby client for VoltDB - one of the fastest in-memory databases on the planet. Threadsafe and fast enough wire client implementation
6
+
7
+ Protocol Version 1 (01/26/2016).
8
+
9
+ ## Reference docs
10
+
11
+ * [Welcome to VoltDB. A Tutorial](http://downloads.voltdb.com/documentation/tutorial.pdf)
12
+ * [VoltDB Client Wire Protocol](http://downloads.voltdb.com/documentation/wireprotocol.pdf)
13
+ * [Using VoltDB](http://downloads.voltdb.com/documentation/UsingVoltDB.pdf)
14
+ * [VoltDB Guide to Performance and Customization](http://downloads.voltdb.com/documentation/PerfGuide.pdf)
15
+
16
+ ## Compatibility
17
+
18
+ Compatible with Ruby **MRI 2.5**> & **JRuby 9.2**>
19
+
20
+ Previous versions can be supported easily, and you can do it yourself forking this code. However, performance *will probably drop* with previous versions or Ruby, which is essential, since we are dealing with a really fast database! On my laptop VoltDB is responding 2-5 times faster than Ruby can dispatch the response within one process and few threads.
21
+
22
+ ## Installation
23
+
24
+ $ gem install ruby_volt
25
+
26
+ ## Initialize
27
+
28
+ Require gem:
29
+
30
+ require 'ruby_volt'
31
+
32
+ Init the client with options:
33
+
34
+ voltdb = RubyVolt::Base.new(options)
35
+
36
+ Where *options* is a Hash with keys:
37
+
38
+ :cluster - Hash, Array, String of VoltDB cluster:
39
+ - String URI: 1 connection to "your.server" or "your.server:21212" or "user:password@your.server:21212",
40
+ - Array of Strings: 1 connection per server ["first.server", "second.server:21213", "user@third.server"],
41
+ - Hash: where keys are Strings and values are numbers of connections to particular servers {"first.server" => 2, "second_server:21213" => 1}
42
+ - Nil: if option is not defined then 1 connection to 'localhost' with default port 21212.
43
+
44
+ :username - default username for connection(s) if needed,
45
+ :password - default password for connection(s) if needed,
46
+ :connections - target number of connections to be enquired, if needed more than 1 by default,
47
+ :servicename - 'database' by default (see VoltDB documentation),
48
+ :connect_timeout - timeout (secs) for authentication, 8 by default,
49
+ :procedure_timeout - timeout (secs) for procedure call, 8 by default,
50
+ :login_protocol - VoltDB Client Wire Protocol version for authentication process, 1 by default,
51
+ :procedure_protocol - VoltDB Client Wire Protocol version for procedure invocation, 0 by default,
52
+
53
+ General example:
54
+
55
+ voltdb = RubyVolt::Base.new(:username => "username", :password => "secret") # established 1 connection to 'localhost@21212'
56
+ => #<RubyVolt::Base:0x00007fb24f151388 @login_protocol=1, @procedure_protocol=0, @servicename="database", @connect_timeout=8, @procedure_timeout=8, @requests_queue=#<Thread::Queue:0x00007fb24f151360>, @connection_pool=[#<RubyVolt::Connection [localhost:21212]: server_host_id=0 connection_id=181 login_protocol=1 procedure_protocol=0>]>
57
+
58
+ ## Usage
59
+
60
+ A client connection to a VoltDB instance consists of a TCP connection on port 21212. After the initial login process the only exchange between the client library and the VoltDB server is the invocation of and response to stored procedures.
61
+
62
+ In other words, *calling stored procedures is the only function* of VoltDB Client Wire Protocol at the moment.
63
+
64
+ For development and testing purposes I created one table named **_datatypes_** consists of all types of data VoltDB operates with and one stored procedure called **_datatypes_** which just fetch few rows from that table:
65
+
66
+ CREATE TABLE datatypes (
67
+ t_byte TINYINT,
68
+ t_short SMALLINT,
69
+ t_integer INTEGER,
70
+ t_long BIGINT,
71
+ t_float FLOAT,
72
+ t_string VARCHAR,
73
+ t_timestamp TIMESTAMP,
74
+ t_decimal DECIMAL,
75
+ t_varbinary VARBINARY,
76
+ t_geopoint GEOGRAPHY_POINT,
77
+ t_polygon GEOGRAPHY
78
+ );
79
+
80
+ CREATE PROCEDURE datatypes
81
+ AS SELECT *
82
+ FROM datatypes;
83
+
84
+ So, calling procedure looks like that:
85
+
86
+ voltdb.call_procedure("datatypes")
87
+ => #<RubyVolt::InvocationResponse:0x00007fb2501191d0 @bytes=<RubyVolt::ReadPartial: bytes=0>, @data={:protocol=>0, :client_data=>"\eX\xE2\x90\xDFa*\x82", :present_fields=>0, :status=>RubyVolt::SuccessStatusCode, :app_status=>nil, :cluster_round_trip_time=>0}, @result=[#<RubyVolt::VoltTable index=0 total_table_length=1846 total_metadata_length=151 rows=5>]>
88
+
89
+ You've got the RubyVolt::InvocationResponse object having some datasets like metadata:
90
+
91
+ voltdb.call_procedure("datatypes").data # Metadata around calling procedure
92
+ => {:protocol=>0, :client_data=>"4q#\xF0S\x8E\x83u", :present_fields=>0, :status=>RubyVolt::SuccessStatusCode, :app_status=>nil, :cluster_round_trip_time=>0}
93
+
94
+ Also it has invocation result itself consists of VoltTable sets:
95
+
96
+ Read [Using VoltDB documentation](http://downloads.voltdb.com/documentation/UsingVoltDB.pdf) explaining why we have an array of VoltTables. In our case we have just one VoltTable.
97
+
98
+ voltdb.call_procedure("datatypes").result
99
+ => [#<RubyVolt::VoltTable index=0 total_table_length=1846 total_metadata_length=151 rows=5>]
100
+
101
+ Each VoltTable has array of rows, we've got *5 rows*:
102
+
103
+ voltdb.call_procedure("datatypes").result[0].rows
104
+ => [#<struct T_BYTE=1, T_SHORT=23, T_INTEGER=19948544, T_LONG=123456789101, T_FLOAT=-23325.23425, T_STRING="Madagaskar", T_TIMESTAMP=2018-12-27 03:00:00 +0300, T_DECIMAL=-0.2332523425e5, T_VARBINARY="", T_GEOPOINT=POINT(-109.8223383 34.9766921), T_POLYGON=POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))>, #<struct T_BYTE=1, T_SHORT=23, T_INTEGER=19948544, T_LONG=123456789101, T_FLOAT=-23325.23425, T_STRING="Madagaskar", T_TIMESTAMP=2018-12-27 03:00:00 +0300, T_DECIMAL=-0.2332523425e5, T_VARBINARY=nil, T_GEOPOINT=POINT(-109.8223383 34.9766921), T_POLYGON=POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))>, #<struct T_BYTE=1, T_SHORT=23, T_INTEGER=19948544, T_LONG=123456789101, T_FLOAT=-23325.23425, T_STRING="", T_TIMESTAMP=2019-12-27 03:00:00 +0300, T_DECIMAL=-0.2322522425e5, T_VARBINARY=nil, T_GEOPOINT=POINT(-110.8223383 35.4766421), T_POLYGON=POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))>, #<struct T_BYTE=1, T_SHORT=23, T_INTEGER=19948544, T_LONG=123456789101, T_FLOAT=-23325.23425, T_STRING=nil, T_TIMESTAMP=2019-12-27 03:00:00 +0300, T_DECIMAL=-0.2322522425e5, T_VARBINARY=nil, T_GEOPOINT=POINT(-110.8223383 35.4766421), T_POLYGON=POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))>, #<struct T_BYTE=nil, T_SHORT=nil, T_INTEGER=nil, T_LONG=nil, T_FLOAT=nil, T_STRING=nil, T_TIMESTAMP=nil, T_DECIMAL=nil, T_VARBINARY=nil, T_GEOPOINT=nil, T_POLYGON=nil>]
105
+
106
+ Each row represented as instance of Ruby's Struct class, having attributes according to column names (case sensitive):
107
+
108
+ voltdb.call_procedure("datatypes").result[0].rows[3]
109
+ => #<struct T_BYTE=1, T_SHORT=23, T_INTEGER=19948544, T_LONG=123456789101, T_FLOAT=-23325.23425, T_STRING=nil, T_TIMESTAMP=2019-12-27 03:00:00 +0300, T_DECIMAL=-0.2322522425e5, T_VARBINARY=nil, T_GEOPOINT=POINT(-110.8223383 35.4766421), T_POLYGON=POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))>
110
+
111
+ voltdb.call_procedure("datatypes").result[0].rows[3].T_FLOAT
112
+ => -23325.23425
113
+
114
+ ## Calling procedures with blocks
115
+
116
+ For deeper integration of the received data, you can send block with calling procedure method, which will allow to obtain data before VoltTable is formed and row is still an *Array* object. So you can just skip some internal operations transforming *Array row* to *Struct row*:
117
+
118
+ voltdb.call_procedure("datatypes") do |data, volttable, row|
119
+ puts volttable.columns.inspect # Do something with columns info
120
+ puts row.inspect # Do something extra with your data here ...
121
+ end
122
+ => #<RubyVolt::InvocationResponse:0x00007fb250141950 @bytes=<RubyVolt::ReadPartial: bytes=0>, @data={:protocol=>0, :client_data=>"1\x11\xEB\x90\xFFS~\x89", :present_fields=>0, :status=>RubyVolt::SuccessStatusCode, :app_status=>nil, :cluster_round_trip_time=>0}, @result=[#<RubyVolt::VoltTable index=0 total_table_length=1846 total_metadata_length=151 rows=0>]>
123
+
124
+ In previous example note that VoltTable's rows are empty (rows=0), but you can reproduce the standard functionality, using *add_struct()* method:
125
+
126
+ voltdb.call_procedure("datatypes") do |data, volttable, row|
127
+ volttable.add_struct(row)
128
+ end
129
+ => #<RubyVolt::InvocationResponse:0x00007fb250851510 @bytes=<RubyVolt::ReadPartial: bytes=0>, @data={:protocol=>0, :client_data=>"\x01-\xD9p\xBE\xA0H\xEA", :present_fields=>0, :status=>RubyVolt::SuccessStatusCode, :app_status=>nil, :cluster_round_trip_time=>0}, @result=[#<RubyVolt::VoltTable index=0 total_table_length=1846 total_metadata_length=151 rows=5>]>
130
+
131
+ ## Сalling procedures Async
132
+
133
+ Frankly speaking, each invocation made *asynchronous* in nature. The difference is that *synchronous* call waits until data has been read.
134
+ But you can split *invocation* and *reading* in timeline, although you can read data just once!
135
+
136
+ async_call = voltdb.async_call_procedure("datatypes")
137
+ => #<RubyVolt::Base::AsyncCall ready=false>
138
+
139
+ ... some other stuff ... time spending ...
140
+
141
+ async_call.dispatch # You can read data just ONCE!
142
+ => #<RubyVolt::InvocationResponse:0x00007fb2500fdcf0 @bytes=<RubyVolt::ReadPartial: bytes=0>, @data={:protocol=>0, :client_data=>"\x04\xB7\xE2\xC0 <*w", :present_fields=>0, :status=>RubyVolt::SuccessStatusCode, :app_status=>nil, :cluster_round_trip_time=>0}, @result=[#<RubyVolt::VoltTable index=0 total_table_length=1846 total_metadata_length=151 rows=5>]>
143
+
144
+ As soon as you read the data, they are erased from the buffer.
145
+ You can operate with the result as described before, using blocks as well.
146
+
147
+ ## Passing parameters
148
+
149
+ See [Using VoltDB docs](http://downloads.voltdb.com/documentation/UsingVoltDB.pdf) how to create stored procedures. Let's say we have procedure named *leastpopulated* which accepts one integer parameter. Pass the parameter next to procedure name:
150
+
151
+ voltdb.call_procedure("leastpopulated", 3)
152
+
153
+ In general, you can pass any type of parameter data as method's argument next to procedure name.
154
+
155
+ RubyVolt converts Ruby datatypes and VoltDB datatypes vice-versa:
156
+
157
+ * Ruby's **Integer** (depends on bit length) - to VoltDB's *Byte*, *Short*, *Integer*, or *Long*
158
+ * Ruby's **Float** - to VoltDB's *Float*
159
+ * Ruby's **String** (depends on encoding) - to VoltDB's *Varbinary* or *String*
160
+ * Ruby's **BigDecimal** - to VoltDB's *Decimal*
161
+ * Ruby's **Time** - to VoltDB's *Timestamp*
162
+ * Ruby's **Array** - to VoltDB's *Array*
163
+
164
+ Two datatypes implemented additionaly, according to the protocol specifications and WKT:
165
+
166
+ * **RubyVolt::Meta::Geography::Point** - to VoltDB's *GeographyPointValue*
167
+ * **RubyVolt::Meta::Geography::Polygon** - to VoltDB's *GeographyValue*
168
+
169
+ See detailed description of these datatypes in [VoltDB Guide to Performance and Customization](http://downloads.voltdb.com/documentation/PerfGuide.pdf) (chapter 6.1. The Geospatial Datatypes).
170
+
171
+ ### Passing Arrays
172
+
173
+ VoltDB Array is a single datatype set, so in Ruby you have to specify datatype as the first element:
174
+
175
+ voltdb.call_procedure("any_other_procedure", [RubyVolt::DataType::Byte, 1,2,3,4,5])
176
+
177
+ or
178
+
179
+ voltdb.call_procedure("any_other_procedure", [:Byte, 1,2,3,4,5])
180
+
181
+ or
182
+
183
+ voltdb.call_procedure("any_other_procedure", ["Byte", 1,2,3,4,5])
184
+
185
+ or let RubyVolt recognize it
186
+
187
+ voltdb.call_procedure("any_other_procedure", [1,2,3,4,5])
188
+
189
+ ## Geospatial Data
190
+
191
+ VoltDB has two data structures for geospatial data:
192
+
193
+ * *GeographyPointValue* represent coordinates
194
+ * *GeographyValue* represents polygons
195
+
196
+ And official documentation says:
197
+ *"It should be noted that, although a description of the GeographyPointValue/GeographyValue structure is being provided here for completeness, in most cases the client interface does not need to interpret the structure. Generally the client passes the point representation unchanged between the server and the client application.*
198
+
199
+ However, for completeness, RubyVolt provides a classes for such structures.
200
+
201
+ ### RubyVolt::Meta::Geography::Point
202
+
203
+ This class accepts two arguments: *longitude* and *latitude*
204
+
205
+ point = RubyVolt::Meta::Geography::Point.new(lng, lat)
206
+
207
+ But in the wire protocol a point is represented by a three dimensional point on the unit sphere. These three dimensional points are called XYZPoints. Each dimension is a double precision IEEE floating point number. The Euclidean length of each XYZPoint must be 1.0.
208
+
209
+ xyz_point = RubyVolt::Meta::Geography::Point.from_XYZPoint(x, y, z) # ex. 1.000000, 0.000000, 0.000000
210
+
211
+ ### RubyVolt::Meta::Geography::Polygon
212
+
213
+ Polygon has Rings,
214
+ Rings has Points,
215
+ Points are represented in XYZ format.
216
+
217
+ For example, we have set of eight points:
218
+
219
+ p1 = RubyVolt::Meta::Geography::Point.from_XYZPoint 1.000000, 0.000000, 0.000000
220
+ p2 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999848, 0.017452, 0.000000
221
+ p3 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999695, 0.017450, 0.017452
222
+ p4 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999848, 0.000000, 0.017452
223
+ p5 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999997, 0.001745, 0.001745
224
+ p6 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999875, 0.001745, 0.015707
225
+ p7 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999753, 0.015705, 0.015707
226
+ p8 = RubyVolt::Meta::Geography::Point.from_XYZPoint 0.999875, 0.015707, 0.001745
227
+
228
+ Now we create the *ring1* from points *p1..p4*:
229
+
230
+ ring1 = RubyVolt::Meta::Geography::Ring.new # can accept array o points as argument
231
+ ring1.add_point(p1)
232
+ ring1.add_point(p2)
233
+ ring1.add_point(p3)
234
+ ring1.add_point(p4)
235
+
236
+ Next we create the *ring2* from points *p5..p8*:
237
+
238
+ ring2 = RubyVolt::Meta::Geography::Ring.new # can accept array o points as argument
239
+ ring2.add_point(p5)
240
+ ring2.add_point(p6)
241
+ ring2.add_point(p7)
242
+ ring2.add_point(p8)
243
+
244
+ Having two *rings* let's create polygon object from them:
245
+
246
+ polygon = RubyVolt::Meta::Geography::Polygon.new
247
+ polygon.add_ring(ring1)
248
+ polygon.add_ring(ring2)
249
+
250
+ Now we can operate with Polygon object within our environment. These objects (*Polygon* and/or *Point*) can be used as procedure parameters as well.
251
+ When we read geospatial data from the database, RubyVolt represents the same data structures for us.
252
+
253
+ voltdb.call_procedure("datatypes").result[0].rows[3].T_GEOPOINT
254
+ => POINT(-110.8223383 35.4766421) # RubyVolt::Meta::Geography::Point object
255
+
256
+ voltdb.call_procedure("datatypes").result[0].rows[3].T_POLYGON
257
+ => POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 1.0, 0.0 0.0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))
258
+
259
+ ## Ping and Benchmark
260
+
261
+ Ping the connection:
262
+
263
+ voltdb.ping # invoke system @Ping procedure
264
+
265
+ To look how fast your connection is on minimal dataset:
266
+
267
+ voltdb.benchmark(100000) # pings 100k times
268
+
269
+ Same methods *ping()* and *benchmark()* can be run for particular connection from the pool (if you have few of them):
270
+
271
+ voltdb.connection_pool[1].ping
272
+ voltdb.connection_pool[1].benchmark(100000)
273
+
274
+ ## Perfomance
275
+
276
+ As said before, VoltDB is responding 2-5 times faster than Ruby can dispatch the result (depends on particular data structures), so in fact Ruby is a bottleneck.
277
+
278
+ By the way, RubyVolt is a threadsafe and only 1 thread serves 1 connection (1 thread/per connection) by default. My testing shows that there is no need to parallelize (*forking process*) or artificially increase the number of threads per connection, because the specific application in the real-world environment, for example **Puma** or **Unicorn**, will do it best of all.
279
+
280
+ Therefore, in multi-threading or multi-processing environment you can squeeze maximum performance you can. In the MRI environment, multi-threading adds little to performance (~ 20% first 2 threads) due to GIL limitations, but multiprocessing (forking) makes this much better.
281
+
282
+ However, multi-threading *JRuby performance* due to the use of all CPU cores in real time, is approximately two times higher than in MRI.
@@ -0,0 +1,33 @@
1
+ require 'uri'
2
+ require 'digest'
3
+ require 'securerandom'
4
+ require 'socket'
5
+ require 'ipaddr'
6
+
7
+ module RubyVolt
8
+ # VoltDB Wire Protocol specs at http://downloads.voltdb.com/documentation/wireprotocol.pdf
9
+
10
+ DEFAULT_HOSTNAME = "localhost"
11
+ DEFAULT_PORT = 21212
12
+ LOGIN_PROTOCOL=1
13
+ PROCEDURE_PROTOCOL=0
14
+ SERVICE_NAME = "database"
15
+ DEFAULT_TIMEOUT = 8
16
+ CONNECT_TIMEOUT = 8
17
+ PROCEDURE_TIMEOUT = 8
18
+
19
+ def self.config(const, value)
20
+ remove_const(const) if const_defined?(const)
21
+ const_set(const, value)
22
+ end
23
+
24
+ end
25
+
26
+ require 'ruby_volt/helper'
27
+ require 'ruby_volt/exceptions'
28
+ require 'ruby_volt/data_type'
29
+ require 'ruby_volt/meta'
30
+ require 'ruby_volt/read_partial'
31
+ require 'ruby_volt/connection'
32
+ require 'ruby_volt/base'
33
+ require 'ruby_volt/volt_table'
@@ -0,0 +1,105 @@
1
+ module RubyVolt
2
+ class Base
3
+
4
+ class AsyncCall
5
+ attr_reader :opaque, :args, :response_msg
6
+
7
+ def initialize(procedure, *parameters)
8
+ @opaque = Helper.uniq_bytes(8) # Not even necessary
9
+ @args = [procedure, @opaque, *parameters]
10
+ @queue = SizedQueue.new(1)
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class} ready=#{!@queue.empty?}>"
15
+ end
16
+
17
+ def accept_response(response_msg)
18
+ @queue.push(response_msg)
19
+ @queue.close
20
+ end
21
+
22
+ def dispatch(&block)
23
+ if @response_msg ||= @queue.pop
24
+ InvocationResponse.new(@response_msg).unpack!(&block)
25
+ end
26
+ end
27
+ end
28
+
29
+ attr_reader :login_protocol, :procedure_protocol, :servicename, :connect_timeout, :procedure_timeout, :connection_pool, :requests_queue
30
+
31
+ def initialize(options = {})
32
+ @login_protocol = options[:login_protocol]||LOGIN_PROTOCOL
33
+ @procedure_protocol = options[:procedure_protocol]||PROCEDURE_PROTOCOL
34
+ @servicename = options[:servicename]||SERVICE_NAME
35
+ @connect_timeout = options[:connect_timeout]||CONNECT_TIMEOUT # timeout (secs) or Nil for authentication (default=8)
36
+ @procedure_timeout = options[:procedure_timeout]||PROCEDURE_TIMEOUT # timeout (secs) or Nil for procedure call (default=8)
37
+ @requests_queue = Queue.new
38
+ @connection_pool = configure_pool(options)
39
+ # You can create the connection to any of the nodes in the database cluster and your stored procedure will be routed appropriately. In fact, you can create connections to multiple nodes on the server and your subsequent requests will be distributed to the various connections.
40
+ @connection_pool.each do |connection|
41
+ Thread.new do
42
+ begin
43
+ while async_call = @requests_queue.pop
44
+ response_msg = connection.call_procedure(*async_call.args)
45
+ async_call.accept_response(response_msg)
46
+ end
47
+ rescue ThreadError
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def async_call_procedure(procedure, *parameters)
54
+ AsyncCall.new(procedure, *parameters).tap do |async_call|
55
+ @requests_queue.push(async_call)
56
+ end
57
+ end
58
+
59
+ def call_procedure(*args, &block)
60
+ async_call_procedure(*args).dispatch(&block)
61
+ end
62
+
63
+ def ping
64
+ call_procedure("@Ping")
65
+ end
66
+
67
+ def benchmark(cycle = 1000)
68
+ Helper.benchmark(cycle) {ping} # call @Ping - system stored procedure & dispatch
69
+ end
70
+
71
+ private
72
+
73
+ def configure_pool(options)
74
+ cluster = case options[:cluster]
75
+ when nil then {DEFAULT_HOSTNAME => 0} # default hostname "localhost"
76
+ when String then {options[:cluster] => 0}
77
+ when Array then Hash[options[:cluster].map {|node| [node, 0]}]
78
+ when Hash then options[:cluster].transform_values {|cnum| cnum.to_i.abs}
79
+ else
80
+ raise(TypeError, "Cluster definition has to be Hash, Array, String or Nil (default)")
81
+ end
82
+
83
+ zero_nodes = cluster.values.count {|cnum| cnum == 0} # non defined connection number
84
+ if zero_nodes > 0
85
+ connections = options[:connections]||1
86
+ connections -= cluster.values.sum # user defined connections
87
+ connections = zero_nodes if zero_nodes > connections
88
+ for_each, extra = *connections.divmod(zero_nodes)
89
+ cluster.transform_values!.with_index do |cnum, i|
90
+ cnum > 0 ? cnum : (i < extra ? for_each + 1 : for_each)
91
+ end
92
+ end
93
+
94
+ cluster.map do |node, per_node|
95
+ uri = URI.parse("//#{node}")
96
+ host = uri.host
97
+ port = uri.port||DEFAULT_PORT # default TCP port 21212
98
+ username = uri.user||options[:username]||"" # authentication user name for connection or Nil
99
+ password = uri.password||options[:password]||"" # authentication password for connection or Nil
100
+ per_node.times.map {Connection.new(self, host, port, username, password)}
101
+ end.flatten
102
+ end
103
+
104
+ end
105
+ end