handlersocket-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.travis.yml +14 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +90 -0
- data/bin/console +14 -0
- data/bin/mock_hs_server +29 -0
- data/bin/setup +7 -0
- data/ext/handlersocket_ext/extconf.rb +2 -0
- data/ext/handlersocket_ext/handlersocket_ext.c +128 -0
- data/ext/handlersocket_ext/socket/send.h +135 -0
- data/handlersocket.gemspec +25 -0
- data/handlersocket.yml.sample +2 -0
- data/lib/handlersocket.rb +58 -0
- data/lib/handlersocket/ext.rb +2 -0
- data/lib/handlersocket/pure.rb +27 -0
- data/lib/handlersocket/version.rb +3 -0
- data/local_handlersocket.yml +2 -0
- data/mysql.yml.sample +3 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: acad8d994a34c1431708bfe35b8a1dc0fbf94597
|
4
|
+
data.tar.gz: cf8d34c2ad01ee30af626d70fc1bbfcd35a242da
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 41b05acfa225c3adcb1b440eb12da25901ba0ff820ab3fe9f0225b0c5bb4d28d39a5aac74d1ff3004036e1ee8b8e464d03a7e86e98391b142fe86d0072e9761b
|
7
|
+
data.tar.gz: 5a8e4d2a8be4d0dae6b92bebf4aa1e56948e306c4e840bb5e020934695f288d6b0a77660152d31f799b535fe961fcd2a8a37b4e4ee797976759b69486d8517ce
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
language: ruby
|
2
|
+
before_install: gem install bundler -v 1.10.6
|
3
|
+
before_script:
|
4
|
+
- cp local_handlersocket.yml handlersocket.yml
|
5
|
+
- cp mysql.yml.sample mysql.yml
|
6
|
+
- bin/mock_hs_server > server_log &
|
7
|
+
after_script:
|
8
|
+
- cat server_log
|
9
|
+
rvm:
|
10
|
+
- 2.2.3
|
11
|
+
- 2.0.0
|
12
|
+
env:
|
13
|
+
- HS_VERSION=pure
|
14
|
+
- HS_VERSION=ext
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Ilya Bylich
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/iliabylich/handlersocket-ruby.svg?branch=master)](https://travis-ci.org/iliabylich/handlersocket-ruby)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/iliabylich/handlersocket-ruby/badges/gpa.svg)](https://codeclimate.com/github/iliabylich/handlersocket-ruby)
|
3
|
+
|
4
|
+
# HandlerSocket
|
5
|
+
|
6
|
+
Ruby/C -based zero dependency implementation of MySQL's HandlerSocket protocol.
|
7
|
+
|
8
|
+
# What is HandlerSocket
|
9
|
+
|
10
|
+
HandlerSocket is a NoSQL plugin for MySQL. It works as a daemon inside the mysqld process, accepting TCP connections, and executing requests from clients. HandlerSocket does not support SQL queries. Instead, it supports simple CRUD operations on tables.
|
11
|
+
|
12
|
+
HandlerSocket can be much faster than mysqld/libmysql in some cases because it has lower CPU, disk, and network overhead:
|
13
|
+
|
14
|
+
To lower CPU usage it does not parse SQL.
|
15
|
+
Next, it batch-processes requests where possible, which further reduces CPU usage and lowers disk usage.
|
16
|
+
Lastly, the client/server protocol is very compact compared to mysql/libmysql, which reduces network usage
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
# If you want to use Ruby-based implementation
|
24
|
+
gem 'handlersocket-rb', require: 'handlersocket/pure'
|
25
|
+
|
26
|
+
# If you want to use C-based implementation
|
27
|
+
gem 'handlersocket-rb', require: 'handlersocket/ext'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install handlersocket-rb
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
First of all, you need to install [handlersocket](http://yoshinorimatsunobu.blogspot.com.by/2010/10/using-mysql-as-nosql-story-for.html) plugin.
|
41
|
+
|
42
|
+
If you are using Percona Server or MariaDB, you already have all necessary libraries.
|
43
|
+
|
44
|
+
``` sql
|
45
|
+
INSTALL PLUGIN handlersocket SONAME 'handlersocket.so';
|
46
|
+
```
|
47
|
+
|
48
|
+
And add the following code to mysql config file to `mysqld` section:
|
49
|
+
```
|
50
|
+
handlersocket_address="127.0.0.1"
|
51
|
+
handlersocket_port="9998"
|
52
|
+
handlersocket_port_wr="9999"
|
53
|
+
```
|
54
|
+
|
55
|
+
Reload MySQL server and run `show processlist;`. You should see a lot of connections from HandlerSocket.
|
56
|
+
|
57
|
+
To create a connection to HandlerSocket run:
|
58
|
+
``` ruby
|
59
|
+
hs = Handlersocket.new('host', port)
|
60
|
+
```
|
61
|
+
|
62
|
+
To open an index run:
|
63
|
+
``` ruby
|
64
|
+
hs.open_index('0', 'hs_test', 'users', 'PRIMARY', ['id', 'email'])
|
65
|
+
# where:
|
66
|
+
# '0' - index name, you should pass it later to query method
|
67
|
+
# 'hs_test' - database name
|
68
|
+
# 'users' - table name
|
69
|
+
# 'PRIMARY' - index name
|
70
|
+
# ['id', 'name'] - columns that you want to read/write
|
71
|
+
```
|
72
|
+
|
73
|
+
To read the data run:
|
74
|
+
``` ruby
|
75
|
+
hs.find('0', '=', ['12'], ['100]'])
|
76
|
+
# This query opens index '0' (primary index from the previous example)
|
77
|
+
# and finds 100 rows with 'indexed value' = '12'
|
78
|
+
# similar to
|
79
|
+
# SELECT id, name FROM hs_test.users WHERE id = 12 LIMIT 100
|
80
|
+
```
|
81
|
+
|
82
|
+
## Benchmarks
|
83
|
+
|
84
|
+
See `rake:benchmark`.
|
85
|
+
|
86
|
+
```
|
87
|
+
Calculating -------------------------------------
|
88
|
+
pure HS 2.000 i/100ms
|
89
|
+
ext HS 3.149k i/100ms
|
90
|
+
mysql2 669.000 i/100ms
|
91
|
+
-------------------------------------------------
|
92
|
+
pure HS 49.979 (± 2.0%) i/s - 250.000
|
93
|
+
ext HS 110.369M (±15.7%) i/s - 467.793M
|
94
|
+
mysql2 5.218M (±16.3%) i/s - 23.869M
|
95
|
+
|
96
|
+
Comparison:
|
97
|
+
ext HS: 110369437.2 i/s
|
98
|
+
mysql2: 5218120.6 i/s - 21.15x slower
|
99
|
+
pure HS: 50.0 i/s - 2208314.91x slower
|
100
|
+
```
|
101
|
+
|
102
|
+
Where:
|
103
|
+
+ `ext HS` is a C-based implementation of HandlerSocket protocol
|
104
|
+
+ `mysql2` is a default mysql2 adapter
|
105
|
+
+ `pure HS` is a Ruby-based implementation of HandlerSocket protocal
|
106
|
+
|
107
|
+
## Development
|
108
|
+
|
109
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
110
|
+
Fill `handlersocket.yml` and `mysql.yml` in a root directory of the project.
|
111
|
+
Run `rake test:db:create` to create a default database that is used in tests.
|
112
|
+
Then, run `rake` to (re)compile the code and run the tests.
|
113
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
114
|
+
|
115
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
116
|
+
To release a new version, update the version number in `version.rb`,
|
117
|
+
and then run `bundle exec rake release`,
|
118
|
+
which will create a git tag for the version,
|
119
|
+
push git commits and tags,
|
120
|
+
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
121
|
+
|
122
|
+
Run `rake benchmark` to compare the performance of Ruby HS/C HS/Mysql2.
|
123
|
+
|
124
|
+
## Contributing
|
125
|
+
|
126
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/iliabylich/handlersocket-ruby.
|
127
|
+
This project is intended to be a safe, welcoming space for collaboration,
|
128
|
+
and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
129
|
+
|
130
|
+
|
131
|
+
## License
|
132
|
+
|
133
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
134
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rake/extensiontask'
|
4
|
+
Rake::ExtensionTask.new('handlersocket_ext')
|
5
|
+
|
6
|
+
require 'bundler/setup'
|
7
|
+
require 'mysql2'
|
8
|
+
mysql_config = YAML.load_file(File.expand_path('../mysql.yml', __FILE__))
|
9
|
+
client = Mysql2::Client.new(mysql_config)
|
10
|
+
|
11
|
+
namespace :test do
|
12
|
+
namespace :db do
|
13
|
+
desc 'Drops tests database'
|
14
|
+
task :drop do
|
15
|
+
client.query("DROP DATABASE IF EXISTS hs_test")
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Prepares test database'
|
19
|
+
task :create do
|
20
|
+
client.query("CREATE DATABASE hs_test")
|
21
|
+
client.query("use hs_test")
|
22
|
+
client.query <<-SQL
|
23
|
+
CREATE TABLE users (
|
24
|
+
id int(11) NOT NULL AUTO_INCREMENT,
|
25
|
+
email varchar(255) NOT NULL DEFAULT '',
|
26
|
+
PRIMARY KEY (id)
|
27
|
+
)
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Seeds test database'
|
32
|
+
task :seed do
|
33
|
+
count = ENV.fetch('SEED_COUNT', 100)
|
34
|
+
1.upto(count) do |i|
|
35
|
+
client.query("INSERT INTO users(email) VALUES ('email#{i}@email')")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'Runs benchmark hs_pure/hs_ext/mysql2'
|
42
|
+
task :benchmark do
|
43
|
+
mysql_config = YAML.load_file(File.expand_path('../mysql.yml', __FILE__))
|
44
|
+
hs_config = YAML.load_file(File.expand_path('../handlersocket.yml', __FILE__))
|
45
|
+
|
46
|
+
require 'bundler/setup'
|
47
|
+
require 'benchmark/ips'
|
48
|
+
|
49
|
+
require 'handlersocket/pure'
|
50
|
+
class Handlersocket
|
51
|
+
alias_method :pure_query, :query
|
52
|
+
undef_method :query
|
53
|
+
end
|
54
|
+
pure_socket = Handlersocket.new(hs_config['host'], hs_config['port'])
|
55
|
+
pure_socket.pure_query(['P', '0', 'hs_test', 'users', 'PRIMARY', 'id', 'col'])
|
56
|
+
|
57
|
+
require 'handlersocket/ext'
|
58
|
+
class Handlersocket
|
59
|
+
alias_method :ext_query, :query
|
60
|
+
undef_method :query
|
61
|
+
end
|
62
|
+
ext_socket = Handlersocket.new(hs_config['host'], hs_config['port'])
|
63
|
+
ext_socket.ext_query(['P', '0', 'hs_test', 'users', 'PRIMARY', 'id', 'col'])
|
64
|
+
|
65
|
+
require 'mysql2'
|
66
|
+
mysql_client = Mysql2::Client.new(mysql_config)
|
67
|
+
|
68
|
+
Benchmark.ips do |x|
|
69
|
+
x.config time: 5, warmup: 2
|
70
|
+
|
71
|
+
x.report 'pure HS' do |times|
|
72
|
+
pure_socket.pure_query(['0', '=', '1', times.to_s, '100'])
|
73
|
+
end
|
74
|
+
|
75
|
+
x.report 'ext HS' do |times|
|
76
|
+
ext_socket.ext_query(['0', '=', '1', times.to_s, '100'])
|
77
|
+
end
|
78
|
+
|
79
|
+
x.report 'mysql2' do |times|
|
80
|
+
mysql_client.query("SELECT * FROM users where id = #{times} LIMIT 100").to_a
|
81
|
+
end
|
82
|
+
|
83
|
+
x.compare!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
require 'rspec/core/rake_task'
|
88
|
+
RSpec::Core::RakeTask.new(:spec)
|
89
|
+
|
90
|
+
task default: [:clobber, :compile, :spec]
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "handlersocket"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/mock_hs_server
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
port = (ENV['HS_PORT'] || '9997').to_i
|
6
|
+
server = TCPServer.new(port)
|
7
|
+
puts "Server is running on 127.0.0.1:#{port}"
|
8
|
+
|
9
|
+
REQ_RES = {
|
10
|
+
%w(P 0 hs_test users PRIMARY id,email) => %w(0 1),
|
11
|
+
%w(P 1 hs_test users PRIMARY id,email) => %w(0 1),
|
12
|
+
%w(P 2 missing-db missing-table missing-idx ) => %w(1 1 open_table),
|
13
|
+
%w(0 = 1 12 100) => %w(0 2 12 email12@email)
|
14
|
+
}
|
15
|
+
|
16
|
+
loop do
|
17
|
+
Thread.new(server.accept) do |client|
|
18
|
+
loop do
|
19
|
+
req = client.gets
|
20
|
+
if req
|
21
|
+
puts "[client=#{client.object_id} REQ] #{req.inspect}"
|
22
|
+
res = REQ_RES[req.chomp.split("\t")] || ["Unsupported"]
|
23
|
+
res = res.join("\t") + "\n"
|
24
|
+
puts "[client=#{client.object_id} RES] #{res.inspect}"
|
25
|
+
client.puts(res)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/bin/setup
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
#include <stdio.h>
|
2
|
+
#include <ruby.h>
|
3
|
+
|
4
|
+
VALUE rb_Handlersocket;
|
5
|
+
|
6
|
+
#ifndef RARRAY_AREF
|
7
|
+
#define RARRAY_AREF(ary,n) (RARRAY_PTR(ary)[n])
|
8
|
+
#endif
|
9
|
+
|
10
|
+
struct SocketData {
|
11
|
+
int socket_desc;
|
12
|
+
char* host;
|
13
|
+
int port;
|
14
|
+
};
|
15
|
+
typedef struct SocketData SOCKET_DATA;
|
16
|
+
|
17
|
+
#include "socket/send.h"
|
18
|
+
|
19
|
+
// A default destructor for a Handlersocket obejct that closes connection
|
20
|
+
//
|
21
|
+
// @param [Data] data
|
22
|
+
// @see SocketData
|
23
|
+
//
|
24
|
+
static void hs_free(SOCKET_DATA *data) {
|
25
|
+
close(data->socket_desc);
|
26
|
+
}
|
27
|
+
|
28
|
+
// An allocator for a Handlersocket object
|
29
|
+
//
|
30
|
+
// @param [Handlersocket] hs
|
31
|
+
//
|
32
|
+
// @return [Data] SocketData
|
33
|
+
//
|
34
|
+
static VALUE hs_alloc(VALUE hs) {
|
35
|
+
SOCKET_DATA *data;
|
36
|
+
data = malloc(sizeof(SOCKET_DATA));
|
37
|
+
return Data_Wrap_Struct(hs, NULL, hs_free, data);
|
38
|
+
}
|
39
|
+
|
40
|
+
// Disconnects passed Handlersocket
|
41
|
+
//
|
42
|
+
// @param [Handlersocket] hs
|
43
|
+
//
|
44
|
+
static VALUE rb_hs_disconnect(VALUE hs) {
|
45
|
+
SOCKET_DATA *data;
|
46
|
+
Data_Get_Struct(hs, SOCKET_DATA, data);
|
47
|
+
hs_free(data);
|
48
|
+
|
49
|
+
return Qnil;
|
50
|
+
}
|
51
|
+
|
52
|
+
// Establishes a connection to HS server
|
53
|
+
//
|
54
|
+
// @param [Handlersocket] hs
|
55
|
+
//
|
56
|
+
// @return [nil]
|
57
|
+
//
|
58
|
+
static VALUE rb_hs_connect(VALUE hs) {
|
59
|
+
SOCKET_DATA *data;
|
60
|
+
struct sockaddr_in server;
|
61
|
+
|
62
|
+
Data_Get_Struct(hs, SOCKET_DATA, data);
|
63
|
+
|
64
|
+
if (data->socket_desc == -1) {
|
65
|
+
hs_free(data);
|
66
|
+
}
|
67
|
+
|
68
|
+
data->socket_desc = socket(AF_INET , SOCK_STREAM , 0);
|
69
|
+
if (data->socket_desc == -1) {
|
70
|
+
rb_warn("Could not create socket");
|
71
|
+
}
|
72
|
+
|
73
|
+
server.sin_addr.s_addr = inet_addr(data->host);
|
74
|
+
server.sin_family = AF_INET;
|
75
|
+
server.sin_port = htons(data->port);
|
76
|
+
|
77
|
+
if (connect(data->socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0) {
|
78
|
+
rb_raise(rb_eStandardError, "HS: connect error");
|
79
|
+
}
|
80
|
+
|
81
|
+
return Qnil;
|
82
|
+
}
|
83
|
+
|
84
|
+
// Re-establishes a connection to HS server
|
85
|
+
//
|
86
|
+
// @param [Handlersocket] hs
|
87
|
+
//
|
88
|
+
// @return [nil]
|
89
|
+
//
|
90
|
+
static VALUE rb_hs_reconnect(VALUE hs) {
|
91
|
+
rb_hs_disconnect(hs);
|
92
|
+
rb_hs_connect(hs);
|
93
|
+
|
94
|
+
return Qnil;
|
95
|
+
}
|
96
|
+
|
97
|
+
// Constructor for a Handlersocket object
|
98
|
+
//
|
99
|
+
// @param [Handlersocket] hs
|
100
|
+
// @param [String] host
|
101
|
+
// @param [Fixnum] port
|
102
|
+
//
|
103
|
+
// @return [nil]
|
104
|
+
//
|
105
|
+
static VALUE rb_hs_initialize(VALUE hs, VALUE host, VALUE port) {
|
106
|
+
SOCKET_DATA *data;
|
107
|
+
Data_Get_Struct(hs, SOCKET_DATA, data);
|
108
|
+
|
109
|
+
data->host = StringValuePtr(host);
|
110
|
+
data->port = NUM2INT(port);
|
111
|
+
data->socket_desc = -1;
|
112
|
+
|
113
|
+
rb_hs_connect(hs);
|
114
|
+
|
115
|
+
return Qnil;
|
116
|
+
}
|
117
|
+
|
118
|
+
|
119
|
+
void Init_handlersocket_ext(void) {
|
120
|
+
rb_Handlersocket = rb_const_get(rb_cObject, rb_intern("Handlersocket"));
|
121
|
+
|
122
|
+
rb_define_alloc_func(rb_Handlersocket, hs_alloc);
|
123
|
+
rb_define_method(rb_Handlersocket, "initialize", rb_hs_initialize, 2);
|
124
|
+
rb_define_method(rb_Handlersocket, "connect", rb_hs_connect, 0);
|
125
|
+
rb_define_method(rb_Handlersocket, "disconnect", rb_hs_disconnect, 0);
|
126
|
+
rb_define_method(rb_Handlersocket, "reconnect", rb_hs_reconnect, 0);
|
127
|
+
rb_define_method(rb_Handlersocket, "query", rb_hs_query, 1);
|
128
|
+
}
|
@@ -0,0 +1,135 @@
|
|
1
|
+
#include <arpa/inet.h>
|
2
|
+
#include <sys/ioctl.h>
|
3
|
+
|
4
|
+
// Writes passed message to a socket
|
5
|
+
//
|
6
|
+
// @param [int] socket_desc
|
7
|
+
// @param [char*] message
|
8
|
+
//
|
9
|
+
#define SOCKET_WRITE(socket_desc, message) send(socket_desc, message, strlen(message) , 0)
|
10
|
+
|
11
|
+
|
12
|
+
// Concatenates a string +s1+ and a string +s2+ with the length +l2+
|
13
|
+
// Result will be placed into +s1+
|
14
|
+
//
|
15
|
+
// @param [char**] s1
|
16
|
+
// @param [char*] s2
|
17
|
+
// @param [int] l2
|
18
|
+
//
|
19
|
+
void concat(char **s1, char *s2, int l2) {
|
20
|
+
int l1;
|
21
|
+
char *res;
|
22
|
+
|
23
|
+
l1 = *s1 ? strlen(*s1) : 0;
|
24
|
+
res = realloc(*s1, l1 + l2 + 1);
|
25
|
+
|
26
|
+
if (res) {
|
27
|
+
memcpy(res + l1, s2, l2);
|
28
|
+
res[l1 + l2] = 0;
|
29
|
+
*s1 = res;
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
// Writes an array of Ruby strings to passed socket
|
34
|
+
//
|
35
|
+
// @param [int] socket_desc
|
36
|
+
// @param [Array<String>] ary
|
37
|
+
//
|
38
|
+
static void hs_write(int socket_desc, VALUE ary) {
|
39
|
+
char *message, *buffer;
|
40
|
+
int i, l1, l2;
|
41
|
+
VALUE item;
|
42
|
+
message = NULL;
|
43
|
+
|
44
|
+
for (i = 0; i < RARRAY_LEN(ary); ++i) {
|
45
|
+
item = RARRAY_AREF(ary, i);
|
46
|
+
buffer = StringValuePtr(item);
|
47
|
+
l1 = message ? strlen(message) : 0;
|
48
|
+
l2 = buffer ? strlen(buffer) : 0;
|
49
|
+
if (l1 == 0) {
|
50
|
+
message = realloc(message, l2);
|
51
|
+
strcpy(message, buffer);
|
52
|
+
} else {
|
53
|
+
message = realloc(message, l1 + l2 + 2);
|
54
|
+
message[l1] = '\t';
|
55
|
+
memcpy(message + l1 + 1, buffer, l2);
|
56
|
+
message[l1 + l2 + 1] = 0;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
l1 = strlen(message);
|
60
|
+
message = realloc(message, l1 + 2);
|
61
|
+
message[l1] = '\n';
|
62
|
+
message[l1 + 1] = 0;
|
63
|
+
|
64
|
+
SOCKET_WRITE(socket_desc, message);
|
65
|
+
}
|
66
|
+
|
67
|
+
// Reads all available data from the passed socket until the new line
|
68
|
+
//
|
69
|
+
// @param [int] socket_desc
|
70
|
+
//
|
71
|
+
// @return [char*] reply from the server
|
72
|
+
//
|
73
|
+
// @raise [StandardError] when socket is closed
|
74
|
+
//
|
75
|
+
static char* hs_read_until_newline(int socket_desc) {
|
76
|
+
int buffer_length = 100, bytes_read = 0;
|
77
|
+
char buf[buffer_length];
|
78
|
+
char *result = NULL;
|
79
|
+
int end_of_line_matched = 0;
|
80
|
+
|
81
|
+
while (!end_of_line_matched) {
|
82
|
+
bytes_read = read(socket_desc, buf, buffer_length);
|
83
|
+
if (!bytes_read) {
|
84
|
+
rb_raise(rb_eStandardError, "HS: closed socket");
|
85
|
+
}
|
86
|
+
concat(&result, buf, bytes_read);
|
87
|
+
if (buf[bytes_read - 1] == '\n') {
|
88
|
+
end_of_line_matched = 1;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
return result;
|
93
|
+
}
|
94
|
+
|
95
|
+
// Parses "\t"-terminated response from the HS server
|
96
|
+
//
|
97
|
+
// @param [char*] raw_response
|
98
|
+
//
|
99
|
+
// @return [Array<String>] parsed reply
|
100
|
+
//
|
101
|
+
static VALUE hs_parse_response(char* raw_response) {
|
102
|
+
VALUE ary;
|
103
|
+
char *token;
|
104
|
+
|
105
|
+
ary = rb_ary_new();
|
106
|
+
|
107
|
+
while ((token = strsep(&raw_response, "\t\n")) != NULL) {
|
108
|
+
if (strlen(token) > 0) {
|
109
|
+
rb_ary_push(ary, rb_str_new2(token));
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
free(raw_response);
|
114
|
+
free(token);
|
115
|
+
|
116
|
+
return ary;
|
117
|
+
}
|
118
|
+
|
119
|
+
// Sends a query to the HS server and parses response
|
120
|
+
//
|
121
|
+
// @param [Handlersocket] hs
|
122
|
+
// @param [Array<String>] req request data
|
123
|
+
//
|
124
|
+
// @return [Array<String>] reply
|
125
|
+
//
|
126
|
+
static VALUE rb_hs_query(VALUE hs, VALUE req) {
|
127
|
+
SOCKET_DATA *data;
|
128
|
+
char *raw_result;
|
129
|
+
|
130
|
+
Data_Get_Struct(hs, SOCKET_DATA, data);
|
131
|
+
|
132
|
+
hs_write(data->socket_desc, req);
|
133
|
+
raw_result = hs_read_until_newline(data->socket_desc);
|
134
|
+
return hs_parse_response(raw_result);
|
135
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'handlersocket/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "handlersocket-rb"
|
8
|
+
spec.version = Handlersocket::VERSION
|
9
|
+
spec.authors = ["Ilya Bylich"]
|
10
|
+
spec.email = ["ibylich@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Zero-dependency implementation of HandlerSocket protocol}
|
13
|
+
spec.homepage = "https://github.com/iliabylich/handlersocket-ruby"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rake-compiler"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "handlersocket/version"
|
2
|
+
|
3
|
+
# Class for establising connection to MySQL's HandlerSocket
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# hs = Handlersocket.new('0.0.0.0', 9999)
|
7
|
+
#
|
8
|
+
class Handlersocket
|
9
|
+
# @param [String] host
|
10
|
+
# @param [Fixnum] port
|
11
|
+
#
|
12
|
+
def initialize(host, port)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
end
|
16
|
+
|
17
|
+
# Sends a query to Handlersocket server and returns a reply
|
18
|
+
#
|
19
|
+
# @param [Array<String>] args
|
20
|
+
#
|
21
|
+
# @return [Array<String>]
|
22
|
+
#
|
23
|
+
def query(*args)
|
24
|
+
# A stub method
|
25
|
+
end
|
26
|
+
|
27
|
+
# Opens an an index for requests
|
28
|
+
#
|
29
|
+
# @param [String] idx_id id of index
|
30
|
+
# @param [String] db_name name of the database
|
31
|
+
# @param [String] tbl_name name of the table
|
32
|
+
# @param [String] idx_name name of the index
|
33
|
+
# @param [Array<String>] columns you want to get
|
34
|
+
#
|
35
|
+
# @return [Array<String>]
|
36
|
+
#
|
37
|
+
def open_index(idx_id, db_name, tbl_name, idx_name, columns)
|
38
|
+
req = ["P", idx_id, db_name, tbl_name, idx_name, columns.join(",")]
|
39
|
+
query(req)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Runs a find statement on HandlerSocket server and returns matched rows
|
43
|
+
#
|
44
|
+
# @param [String] idx_id id of index
|
45
|
+
# @param [String] op operation ('=', '>', '<')
|
46
|
+
# @param [Array<String>] args arguments for comparison
|
47
|
+
# @param [Array<String>] extra arguments like limit and offset
|
48
|
+
#
|
49
|
+
# @return [Array<String>] matched rows
|
50
|
+
#
|
51
|
+
def find(idx_id, op, args, extra = [])
|
52
|
+
req = [idx_id, op, args.length.to_s, *args, *extra]
|
53
|
+
query(req)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# require 'handlersocket/pure'
|
58
|
+
# require 'handlersocket/ext'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'handlersocket'
|
3
|
+
|
4
|
+
# Pure Ruby implementation of HS protocol
|
5
|
+
#
|
6
|
+
# Sockets implementation is very slow in MRI, use HandlerSocket::Ext for better performance
|
7
|
+
#
|
8
|
+
class Handlersocket
|
9
|
+
# Returns a memoized instance of TCP socket
|
10
|
+
#
|
11
|
+
# @return [TCPSocket]
|
12
|
+
#
|
13
|
+
def socket
|
14
|
+
@socket ||= TCPSocket.new(@host, @port)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Sends request to a HS server and returns a reply
|
18
|
+
#
|
19
|
+
# @param [Array<String>] req
|
20
|
+
#
|
21
|
+
# @return [Array<String>]
|
22
|
+
#
|
23
|
+
def query(req)
|
24
|
+
socket.puts(req.join("\t"))
|
25
|
+
socket.gets.chomp.split("\t")
|
26
|
+
end
|
27
|
+
end
|
data/mysql.yml.sample
ADDED
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: handlersocket-rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ilya Bylich
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake-compiler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- ibylich@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".travis.yml"
|
78
|
+
- CODE_OF_CONDUCT.md
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/console
|
84
|
+
- bin/mock_hs_server
|
85
|
+
- bin/setup
|
86
|
+
- ext/handlersocket_ext/extconf.rb
|
87
|
+
- ext/handlersocket_ext/handlersocket_ext.c
|
88
|
+
- ext/handlersocket_ext/socket/send.h
|
89
|
+
- handlersocket.gemspec
|
90
|
+
- handlersocket.yml.sample
|
91
|
+
- lib/handlersocket.rb
|
92
|
+
- lib/handlersocket/ext.rb
|
93
|
+
- lib/handlersocket/pure.rb
|
94
|
+
- lib/handlersocket/version.rb
|
95
|
+
- local_handlersocket.yml
|
96
|
+
- mysql.yml.sample
|
97
|
+
homepage: https://github.com/iliabylich/handlersocket-ruby
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata: {}
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 2.4.8
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Zero-dependency implementation of HandlerSocket protocol
|
121
|
+
test_files: []
|