tarantool 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "eventmachine"
4
+ gem "em-synchrony"
5
+ gem "activemodel"
6
+
7
+ gem "rr"
8
+ gem "yajl-ruby"
9
+ gem "bson"
10
+ gem "bson_ext"
11
+
12
+ gem "ruby-debug19"
13
+
14
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,55 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tarantool (0.1)
5
+ activemodel (>= 3.1, < 4.0)
6
+ em-synchrony (>= 1.0.0, < 2.0)
7
+ eventmachine (>= 1.0.0.beta.4, < 2.0.0)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activemodel (3.1.1)
13
+ activesupport (= 3.1.1)
14
+ builder (~> 3.0.0)
15
+ i18n (~> 0.6)
16
+ activesupport (3.1.1)
17
+ multi_json (~> 1.0)
18
+ archive-tar-minitar (0.5.2)
19
+ bson (1.4.0)
20
+ bson_ext (1.4.0)
21
+ builder (3.0.0)
22
+ columnize (0.3.5)
23
+ em-synchrony (1.0.0)
24
+ eventmachine (>= 1.0.0.beta.1)
25
+ eventmachine (1.0.0.beta.4)
26
+ i18n (0.6.0)
27
+ linecache19 (0.5.12)
28
+ ruby_core_source (>= 0.1.4)
29
+ multi_json (1.0.3)
30
+ rr (1.0.4)
31
+ ruby-debug-base19 (0.11.25)
32
+ columnize (>= 0.3.1)
33
+ linecache19 (>= 0.5.11)
34
+ ruby_core_source (>= 0.1.4)
35
+ ruby-debug19 (0.11.6)
36
+ columnize (>= 0.3.1)
37
+ linecache19 (>= 0.5.11)
38
+ ruby-debug-base19 (>= 0.11.19)
39
+ ruby_core_source (0.1.5)
40
+ archive-tar-minitar (>= 0.5.2)
41
+ yajl-ruby (1.1.0)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ activemodel
48
+ bson
49
+ bson_ext
50
+ em-synchrony
51
+ eventmachine
52
+ rr
53
+ ruby-debug19
54
+ tarantool!
55
+ yajl-ruby
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright (C) 2011 Mail.RU
3
+ *
4
+ * Redistribution and use in source and binary forms, with or without
5
+ * modification, are permitted provided that the following conditions
6
+ * are met:
7
+ * 1. Redistributions of source code must retain the above copyright
8
+ * notice, this list of conditions and the following disclaimer.
9
+ * 2. Redistributions in binary form must reproduce the above copyright
10
+ * notice, this list of conditions and the following disclaimer in the
11
+ * documentation and/or other materials provided with the distribution.
12
+ *
13
+ * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
+ * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
17
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23
+ * SUCH DAMAGE.
24
+ */
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # About
2
+
3
+ Its asynchronyous EventMachine ruby client for [Tarantool Key-Value Storage](github.com/mailru/tarantool).
4
+
5
+ # Install
6
+
7
+ ```bash
8
+ gem install tarantool
9
+ ```
10
+
11
+ ```ruby
12
+ require 'tarantool'
13
+ # or
14
+ require 'tarantool/synchrony'
15
+ # or
16
+ require 'tarantool/record'
17
+ ```
18
+
19
+ # Usage
20
+
21
+ Before all requests you must configure client:
22
+
23
+ ```ruby
24
+ Tarantool.configure host: 'locahost', port: 33013, space_no: 0
25
+ ```
26
+
27
+ Low level part of client can work in two mode: EM deferrables and EM-Synchrony.
28
+
29
+ EM deferrables:
30
+
31
+ ```ruby
32
+ req = Tarantool.insert 'prepor', 'Andrew', 'ceo@prepor.ru'
33
+ req.callback do
34
+ req = Tarantool.select 'prepor'
35
+ req.callback do |res|
36
+ puts "Name: #{res.tuple[1].to_s}; Email: #{res.tuple[2].to_s}"
37
+ end
38
+ end
39
+ req.errback do |err|
40
+ puts "Error while insert: #{err}"
41
+ end
42
+ ```
43
+
44
+ Synchrony mode:
45
+
46
+ ```ruby
47
+ require 'tarantool/synchrony'
48
+
49
+ Tarantool.insert 'prepor', 'Andrew', 'ceo@prepor.ru'
50
+ res = Tarantool.select 'prepor'
51
+ puts "Name: #{res.tuple[1].to_s}; Email: #{res.tuple[2].to_s}"
52
+ ```
53
+
54
+ High level part of client implements ActiveModel API: Callbacks, Validations, Serialization, Dirty. Its autocast types, choose right index for query and something more. So code looks like this:
55
+
56
+ ```ruby
57
+ require 'tarantool/record'
58
+ require 'tarantool/serializers/bson'
59
+ class User < Tarantool::Record
60
+ field :login, :string
61
+ field :name, :string
62
+ field :email, :string
63
+ field :apples_count, :integer, default: 0
64
+ field :info, :bson
65
+ index :name, :email
66
+
67
+ validates_length_of(:login, minimum: 3)
68
+
69
+ after_create do
70
+ # after work!
71
+ end
72
+ end
73
+
74
+ # Now attribute positions are not important.
75
+ User.create login: 'prepor', email: 'ceo@prepor.ru', name: 'Andrew'
76
+ User.create login: 'ruden', name: 'Andrew', email: 'rudenkoco@gmail.com'
77
+
78
+ # find by primary key login
79
+ User.find 'prepor'
80
+ # first 2 users with name Andrew
81
+ User.where(name: 'Andrew').limit(2).all
82
+ # second user with name Andrew
83
+ User.where(name: 'Andrew').offset(1).limit(1).all
84
+ # user with name Andrew and email ceo@prepor.ru
85
+ User.where(name: 'Andrew', email: 'ceo@prepor.ru').first
86
+ # raise exception, becouse we can't select query started from not first part of index
87
+ begin
88
+ User.where(email: 'ceo@prepor.ru')
89
+ rescue Tarantool::ArgumentError => e
90
+ end
91
+ # increment field apples_count by one. Its atomic operation via native Tarantool interface
92
+ User.find('prepor').increment :apples_count
93
+
94
+ # update only dirty attributes
95
+ user = User.find('prepor')
96
+ user.name = "Petr"
97
+ user.save
98
+
99
+ # field serialization to bson
100
+ user.info = { 'bio' => "hi!", 'age' => 23, 'hobbies' => ['mufa', 'tuka'] }
101
+ user.save
102
+ User.find('prepor').info['bio'] # => 'hi!'
103
+ # delete record
104
+ user.destroy
105
+ ```
106
+
107
+ On record definition step, fields order are important, in that order they will be store in Tarantool. By default primary key is first field. `index` method just mapping to your Tarantool schema, client doesn't modify schema for you.
108
+
109
+ # TODO
110
+
111
+ * `#first`, `#all` without keys, batches requests via box.select_range
112
+ * `#where` chains
113
+ * admin-socket protocol
114
+ * safe to add fields to exist model
115
+ * Hash, Array and lambdas as default values
116
+ * timers to response, reconnect strategies
data/Rakefile ADDED
@@ -0,0 +1,131 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+
47
+ task :default => :spec
48
+ require 'rake/testtask'
49
+ Rake::TestTask.new(:spec) do |t|
50
+ t.libs << 'spec'
51
+ t.pattern = 'spec/**/*_spec.rb'
52
+ t.verbose = false
53
+ end
54
+
55
+ desc "Generate RCov test coverage and open in your browser"
56
+ task :coverage do
57
+ require 'rcov'
58
+ sh "rm -fr coverage"
59
+ sh "rcov test/test_*.rb"
60
+ sh "open coverage/index.html"
61
+ end
62
+
63
+ desc "Open an irb session preloaded with this library"
64
+ task :console do
65
+ sh "irb -rubygems -r ./lib/#{name}.rb"
66
+ end
67
+
68
+
69
+ #############################################################################
70
+ #
71
+ # Packaging tasks
72
+ #
73
+ #############################################################################
74
+
75
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
76
+ task :release => :build do
77
+ unless `git branch` =~ /^\* master$/
78
+ puts "You must be on the master branch to release!"
79
+ exit!
80
+ end
81
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
82
+ sh "git tag v#{version}"
83
+ sh "git push origin master"
84
+ sh "git push origin v#{version}"
85
+ sh "gem push pkg/#{name}-#{version}.gem"
86
+ end
87
+
88
+ desc "Build #{gem_file} into the pkg directory"
89
+ task :build => :gemspec do
90
+ sh "mkdir -p pkg"
91
+ sh "gem build #{gemspec_file}"
92
+ sh "mv #{gem_file} pkg"
93
+ end
94
+
95
+ desc "Generate #{gemspec_file}"
96
+ task :gemspec => :validate do
97
+ # read spec file and split out manifest section
98
+ spec = File.read(gemspec_file)
99
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
100
+
101
+ # replace name version and date
102
+ replace_header(head, :name)
103
+ replace_header(head, :version)
104
+ replace_header(head, :date)
105
+ #comment this out if your rubyforge_project has a different name
106
+ replace_header(head, :rubyforge_project)
107
+
108
+ # determine file list from git ls-files
109
+ files = `git ls-files`.
110
+ split("\n").
111
+ sort.
112
+ reject { |file| file =~ /^\./ }.
113
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
114
+ map { |file| " #{file}" }.
115
+ join("\n")
116
+
117
+ # piece file back together and write
118
+ manifest = " s.files = %w[\n#{files}\n ]\n"
119
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
120
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
121
+ puts "Updated #{gemspec_file}"
122
+ end
123
+
124
+ desc "Validate #{gemspec_file}"
125
+ task :validate do
126
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
127
+ unless Dir['VERSION*'].empty?
128
+ puts "A `VERSION` file at root level violates Gem best practices."
129
+ exit!
130
+ end
131
+ end
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
3
+ Bundler.setup
4
+
5
+ require 'tarantool'
6
+
7
+ EM.run do
8
+ Tarantool.configure host: 'localhost', port: 33013, space_no: 0
9
+ req = Tarantool.insert 'prepor', 'Andrew', 'ceo@prepor.ru'
10
+ req.callback do
11
+ req = Tarantool.select 'prepor'
12
+ req.callback do |res|
13
+ puts "Name: #{res.tuple[1].to_s}; Email: #{res.tuple[2].to_s}"
14
+ EM.stop
15
+ end
16
+ end
17
+ req.errback do |err|
18
+ puts "Error while insert: #{err}"
19
+ EM.stop
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ require 'bundler'
2
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
3
+ Bundler.setup
4
+
5
+ require 'tarantool/record'
6
+ require 'tarantool/serializers/bson'
7
+ class User < Tarantool::Record
8
+ field :login, :string
9
+ field :name, :string
10
+ field :email, :string
11
+ field :apples_count, :integer, default: 0
12
+ field :info, :bson
13
+ index :name, :email
14
+
15
+ validates_length_of(:login, minimum: 3)
16
+
17
+ after_create do
18
+ # after work!
19
+ end
20
+ end
21
+
22
+ EM.synchrony do
23
+ Tarantool.configure host: 'localhost', port: 33013, space_no: 0
24
+ # Now attribute positions are not important.
25
+ User.create login: 'prepor', email: 'ceo@prepor.ru', name: 'Andrew'
26
+ User.create login: 'ruden', name: 'Andrew', email: 'rudenkoco@gmail.com'
27
+
28
+ # find by primary key login
29
+ User.find 'prepor'
30
+ # first 2 users with name Andrew
31
+ User.where(name: 'Andrew').limit(2).all
32
+ # second user with name Andrew
33
+ User.where(name: 'Andrew').offset(1).limit(1).all
34
+ # user with name Andrew and email ceo@prepor.ru
35
+ User.where(name: 'Andrew', email: 'ceo@prepor.ru').first
36
+ # raise exception, becouse we can't select from not first part of index
37
+ begin
38
+ User.where(email: 'ceo@prepor.ru')
39
+ rescue Tarantool::ArgumentError => e
40
+ end
41
+ # increment field apples_count by one. Its atomic operation vie native Tarantool interface
42
+ User.find('prepor').increment :apples_count
43
+
44
+ # update only dirty attributes
45
+ user = User.find('prepor')
46
+ user.name = "Petr"
47
+ user.save
48
+
49
+ # field serialization to bson
50
+ user.info = { 'bio' => "hi!", 'age' => 23, 'hobbies' => ['mufa', 'tuka'] }
51
+ user.save
52
+ User.find('prepor').info['bio'] # => 'hi!'
53
+ user.destroy
54
+ puts "Ok!"
55
+ EM.stop
56
+ end
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
3
+ Bundler.setup
4
+
5
+ require 'tarantool/synchrony'
6
+
7
+ EM.synchrony do
8
+ Tarantool.configure host: 'localhost', port: 33013, space_no: 0
9
+ Tarantool.insert 'prepor', 'Andrew', 'ceo@prepor.ru'
10
+ res = Tarantool.select 'prepor'
11
+ puts "Name: #{res.tuple[1].to_s}; Email: #{res.tuple[2].to_s}"
12
+ EM.stop
13
+ end
@@ -0,0 +1,67 @@
1
+ module EventMachine
2
+ module Protocols
3
+ module FixedHeaderAndBody
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def header_size(size = nil)
11
+ if size
12
+ @_header_size = size
13
+ else
14
+ @_header_size
15
+ end
16
+ end
17
+ end
18
+
19
+ attr_accessor :header, :body
20
+
21
+ def receive_data(data)
22
+ @buffer ||= ''
23
+ offset = 0
24
+ while (chunk = data[offset, _needed_size - @buffer.size]).size > 0 || _needed_size == 0
25
+ @buffer += chunk
26
+ offset += chunk.size
27
+ if @buffer.size == _needed_size
28
+ case _state
29
+ when :receive_header
30
+ @_state = :receive_body
31
+ receive_header @buffer
32
+ when :receive_body
33
+ @_state = :receive_header
34
+ receive_body @buffer
35
+ end
36
+ @buffer = ''
37
+ end
38
+ end
39
+ end
40
+
41
+ def receive_header(header)
42
+ # for override
43
+ end
44
+
45
+ def body_size
46
+ # for override
47
+ end
48
+
49
+ def receive_body(body)
50
+ # for override
51
+ end
52
+
53
+ def _needed_size
54
+ case _state
55
+ when :receive_header
56
+ self.class.header_size
57
+ when :receive_body
58
+ body_size
59
+ end
60
+ end
61
+
62
+ def _state
63
+ @_state ||= :receive_header
64
+ end
65
+ end
66
+ end
67
+ end