riaktor 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -1
- data/Manifest.txt +14 -2
- data/lib/riaktor/client.rb +137 -0
- data/lib/riaktor/document/association.rb +123 -0
- data/lib/riaktor/document/associations.rb +34 -0
- data/lib/riaktor/document/attribute.rb +13 -0
- data/lib/riaktor/document/attributes.rb +79 -0
- data/lib/riaktor/document/base.rb +124 -0
- data/lib/riaktor/document/index.rb +13 -0
- data/lib/riaktor/document/indexes.rb +104 -0
- data/lib/riaktor/document.rb +6 -0
- data/lib/riaktor/r_object/link.rb +44 -0
- data/lib/riaktor/r_object/persistence.rb +155 -0
- data/lib/riaktor/r_object.rb +46 -0
- data/lib/riaktor.rb +1 -1
- metadata +16 -4
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
History.txt
|
2
|
+
lib/riaktor/client.rb
|
3
|
+
lib/riaktor/document/association.rb
|
4
|
+
lib/riaktor/document/associations.rb
|
5
|
+
lib/riaktor/document/attribute.rb
|
6
|
+
lib/riaktor/document/attributes.rb
|
7
|
+
lib/riaktor/document/base.rb
|
8
|
+
lib/riaktor/document/index.rb
|
9
|
+
lib/riaktor/document/indexes.rb
|
10
|
+
lib/riaktor/document.rb
|
11
|
+
lib/riaktor/r_object/link.rb
|
12
|
+
lib/riaktor/r_object/persistence.rb
|
13
|
+
lib/riaktor/r_object.rb
|
14
|
+
lib/riaktor.rb
|
2
15
|
Manifest.txt
|
3
16
|
PostInstall.txt
|
4
|
-
README.rdoc
|
5
17
|
Rakefile
|
6
|
-
|
18
|
+
README.rdoc
|
7
19
|
script/console
|
8
20
|
script/destroy
|
9
21
|
script/generate
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Riaktor
|
2
|
+
class Client
|
3
|
+
class MissingOperationKey < RuntimeError; end
|
4
|
+
|
5
|
+
def self.config
|
6
|
+
@config ||= { :host => "127.0.0.1", :port => 8098, :prefix => 'riak' }
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.config=(hash)
|
10
|
+
@config = hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.client_id
|
14
|
+
Thread.current[:riaktor_client_id] ||=UUIDTools::UUID.random_create.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.client_id=(val)
|
18
|
+
Thread.current[:riaktor_client_id] = val
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.max_concurrency
|
22
|
+
@max_concurrency ||= 25
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.max_concurrency=(val)
|
26
|
+
@max_concurrency = val
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_url(bucket,key=nil)
|
30
|
+
url = []
|
31
|
+
url << "http://#{self.config[:host]}:#{self.config[:port]}"
|
32
|
+
url << self.config[:prefix]
|
33
|
+
url << URI.escape(bucket).gsub("/", "%2F")
|
34
|
+
url << URI.escape(key).gsub("/", "%2F") if key
|
35
|
+
url.join("/")
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.bucket_props(bucket,params={})
|
39
|
+
params[:keys] ||= false
|
40
|
+
response = Typhoeus::Request.get(self.build_url(bucket), :params => params)
|
41
|
+
Yajl::Parser.parse(response.body)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.set_bucket_props(bucket, props, params={})
|
45
|
+
body = Yajl::Encoder.encode(props)
|
46
|
+
response = Typhoeus::Request.put(self.build_url(bucket),
|
47
|
+
:params => params, :headers => { "Content-Type" => "application/json" }, :body => body)
|
48
|
+
response.code == 204
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.put(op)
|
52
|
+
self.put_many([op])[[op["bucket"],op["key"]]]
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.put_many(operations)
|
56
|
+
operations.each do |operation|
|
57
|
+
operation['bucket'] || raise(MissingOperationKey, "bucket")
|
58
|
+
operation['key'] || raise(MissingOperationKey, "key")
|
59
|
+
operation['value'] ||= ""
|
60
|
+
operation['headers'] ||= {}
|
61
|
+
operation['params'] ||= {}
|
62
|
+
end
|
63
|
+
results = {}
|
64
|
+
hydra = Typhoeus::Hydra.new(:max_concurrency => self.max_concurrency)
|
65
|
+
operations.each do |op|
|
66
|
+
op['params']["r"] ||= 2
|
67
|
+
op['params']["w"] ||= 2
|
68
|
+
op['params']["dw"] ||= 0
|
69
|
+
op['params']["returnbody"] ||= true
|
70
|
+
op['headers']["Content-Type"] ||= "text/plain"
|
71
|
+
op['headers']["X-Riak-ClientId"] ||= self.client_id
|
72
|
+
request = Typhoeus::Request.new(self.build_url(op['bucket'],op['key']),
|
73
|
+
:method => :put, :headers => op['headers'], :params => op['params'], :body => op['value'].to_s)
|
74
|
+
request.on_complete do |response|
|
75
|
+
results[[op["bucket"],op["key"]]] = { "headers" => response.headers_hash,
|
76
|
+
"body" => response.body, "code" => response.code }
|
77
|
+
end
|
78
|
+
hydra.queue request
|
79
|
+
end # operations.each
|
80
|
+
hydra.run; results
|
81
|
+
end # self.put_many
|
82
|
+
|
83
|
+
def self.get(op)
|
84
|
+
self.get_many([op])[[op["bucket"],op["key"]]]
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.get_many(operations)
|
88
|
+
operations.each do |operation|
|
89
|
+
operation['bucket'] || raise(MissingOperationKey, "bucket")
|
90
|
+
operation['key'] || raise(MissingOperationKey, "key")
|
91
|
+
operation['headers'] ||= {}
|
92
|
+
operation['params'] ||= {}
|
93
|
+
end
|
94
|
+
results = {}
|
95
|
+
hydra = Typhoeus::Hydra.new(:max_concurrency => self.max_concurrency)
|
96
|
+
operations.each do |op|
|
97
|
+
op['params']["r"] ||= 2
|
98
|
+
op['headers']["Content-Type"] ||= "text/plain"
|
99
|
+
op['headers']["X-Riak-ClientId"] ||= self.client_id
|
100
|
+
request = Typhoeus::Request.new(self.build_url(op['bucket'],op['key']),
|
101
|
+
:method => :get, :headers => op['headers'], :params => op['params'])
|
102
|
+
request.on_complete do |response|
|
103
|
+
results[[op["bucket"], op["key"]]] = { "headers" => response.headers_hash,
|
104
|
+
"body" => response.body, "code" => response.code }
|
105
|
+
end
|
106
|
+
hydra.queue request
|
107
|
+
end # operations.each
|
108
|
+
hydra.run; results
|
109
|
+
end # self.get_many
|
110
|
+
|
111
|
+
def self.delete(op)
|
112
|
+
self.delete_many([op])[[op["bucket"],op["key"]]]
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.delete_many(operations)
|
116
|
+
operations.each do |operation|
|
117
|
+
operation['bucket'] || raise(MissingOperationKey, "bucket")
|
118
|
+
operation['key'] || raise(MissingOperationKey, "key")
|
119
|
+
operation['headers'] ||= {}
|
120
|
+
operation['params'] ||= {}
|
121
|
+
end
|
122
|
+
results = {}
|
123
|
+
hydra = Typhoeus::Hydra.new(:max_concurrency => self.max_concurrency)
|
124
|
+
operations.each do |op|
|
125
|
+
op['headers']["X-Riak-ClientId"] ||= self.client_id
|
126
|
+
request = Typhoeus::Request.new(self.build_url(op['bucket'],op['key']),
|
127
|
+
:method => :delete, :headers => op['headers'], :params => op['params'])
|
128
|
+
request.on_complete do |response|
|
129
|
+
results[[op["bucket"], op["key"]]] = { "headers" => response.headers_hash,
|
130
|
+
"body" => response.body, "code" => response.code }
|
131
|
+
end
|
132
|
+
hydra.queue request
|
133
|
+
end # operations.each
|
134
|
+
hydra.run; results
|
135
|
+
end # self.delete_many
|
136
|
+
end # Client
|
137
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Riaktor
|
2
|
+
module Document
|
3
|
+
class Association
|
4
|
+
attr_accessor :name, :opts, :parent
|
5
|
+
|
6
|
+
def initialize(name, opts, parent)
|
7
|
+
raise(ArgumentError, "missing or invalid name") if name.blank?
|
8
|
+
raise(ArgumentError, "missing or invalid option :class") if opts.blank? or opts[:class].blank?
|
9
|
+
raise(ArgumentError, "missing or invalid parent") if parent.blank?
|
10
|
+
@name = name
|
11
|
+
@opts = opts
|
12
|
+
@parent = parent
|
13
|
+
end
|
14
|
+
|
15
|
+
def klass
|
16
|
+
@klass ||= eval(opts[:class].to_s.camelize)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reload
|
20
|
+
@links = nil
|
21
|
+
@members = nil
|
22
|
+
@sort_index = nil
|
23
|
+
@include = nil
|
24
|
+
@cached_doc = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def changed?
|
28
|
+
@changed == true
|
29
|
+
end
|
30
|
+
|
31
|
+
def links
|
32
|
+
@links ||= begin
|
33
|
+
(@parent.links || []).select { |l| l.tag.to_s == @name.to_s }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def include?(key_or_doc)
|
38
|
+
@include ||= {}
|
39
|
+
@include[key_or_doc] ||= begin
|
40
|
+
if key_or_doc.is_a?(String)
|
41
|
+
links.any? { |l| l.key == key_or_doc }
|
42
|
+
else
|
43
|
+
links.any? { |l| l.key == key_or_doc.key }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def members(from=0,to=-1)
|
49
|
+
@members ||= {}
|
50
|
+
@members[[from..to]] ||= begin
|
51
|
+
if links.empty?
|
52
|
+
[]
|
53
|
+
else
|
54
|
+
if range = links[from..to] and !range.empty?
|
55
|
+
found = range.collect do |link|
|
56
|
+
cached_doc(link.key) || link.key
|
57
|
+
end
|
58
|
+
uncached = found.select { |f| f.is_a?(String) }
|
59
|
+
unless uncached.empty?
|
60
|
+
klass.find_many(uncached).each do |key,doc|
|
61
|
+
cache_doc(key,doc)
|
62
|
+
found[found.index(doc.key)] = doc
|
63
|
+
end
|
64
|
+
end
|
65
|
+
found.sort! { |x,y| sort_index.index(x.key) <=> sort_index.index(y.key) }
|
66
|
+
else
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def <<(doc)
|
74
|
+
@changed = true
|
75
|
+
unless doc.class.to_s == klass.to_s
|
76
|
+
raise(ArgumentError, "invalid object class: #{doc.class.to_s}")
|
77
|
+
end
|
78
|
+
return true if members.include?(doc)
|
79
|
+
link = Riaktor::RObject::Link.new(doc.bucket, doc.key, @name.to_s)
|
80
|
+
@parent.links << link
|
81
|
+
reload
|
82
|
+
true
|
83
|
+
end
|
84
|
+
|
85
|
+
def remove(doc)
|
86
|
+
@changed = true
|
87
|
+
@parent.links.reject! { |l| l.bucket == doc.bucket && l.key == doc.key }
|
88
|
+
reload
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def save
|
93
|
+
if res = @parent.save
|
94
|
+
@changed = false
|
95
|
+
end; res
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
def cached_doc(key)
|
101
|
+
(@cached_doc || {})[key]
|
102
|
+
end
|
103
|
+
|
104
|
+
def cache_doc(key,doc)
|
105
|
+
@cached_doc ||= {}
|
106
|
+
@cached_doc[key] = doc
|
107
|
+
end
|
108
|
+
|
109
|
+
def sort_index
|
110
|
+
@sort_index ||= links.collect { |l| l.key }
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_to_sort_index(key)
|
114
|
+
sort_index
|
115
|
+
@sort_index << key unless @sort_index.include?(key)
|
116
|
+
end
|
117
|
+
|
118
|
+
def remove_from_sort_index(key)
|
119
|
+
sort_index && @sort_index.delete(key)
|
120
|
+
end
|
121
|
+
end # Attribute
|
122
|
+
end # Document
|
123
|
+
end # Riaktor
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/association"
|
2
|
+
|
3
|
+
module Riaktor
|
4
|
+
module Document
|
5
|
+
module Associations
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def defined_associations
|
13
|
+
@defined_associations || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def association(name,opts={})
|
17
|
+
raise(ArgumentError, "missing key :class") unless opts[:class]
|
18
|
+
@defined_associations ||= {}
|
19
|
+
@defined_associations[name] = opts
|
20
|
+
define_method("#{name}") do
|
21
|
+
@associations ||= {}
|
22
|
+
@associations[name] ||= Association.new(name,opts,self)
|
23
|
+
end # define_method
|
24
|
+
end # def association
|
25
|
+
end # ClassMethods
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
def any_associations_changed?
|
29
|
+
(@associations || {}).any? { |k,v| v.changed? }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end # Attributes
|
33
|
+
end # Document
|
34
|
+
end # Riaktor
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/attribute"
|
2
|
+
|
3
|
+
module Riaktor
|
4
|
+
module Document
|
5
|
+
module Attributes
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def defined_attributes
|
13
|
+
@defined_attributes || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute(name,opts={})
|
17
|
+
@defined_attributes ||= {}
|
18
|
+
@defined_attributes[name] = opts
|
19
|
+
define_method("#{name}") do
|
20
|
+
load_attributes && @attributes[name]
|
21
|
+
end # define_method
|
22
|
+
end # def attribute
|
23
|
+
end # ClassMethods
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
def key_for_attribute(name)
|
27
|
+
[self.key, "attribute", name].join("--")
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes_key_to_name_map
|
31
|
+
@attributes_key_to_name_map ||= begin
|
32
|
+
map = {}
|
33
|
+
self.class.defined_attributes.each do |name, opts|
|
34
|
+
map[key_for_attribute(name)] = name
|
35
|
+
end; map
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def any_attributes_in_conflict?
|
40
|
+
load_attributes
|
41
|
+
@attributes.each do |name, attrib|
|
42
|
+
return true if attrib.in_conflict?
|
43
|
+
end; false
|
44
|
+
end
|
45
|
+
|
46
|
+
def load_attributes(force=false)
|
47
|
+
return true if @attributes and !force
|
48
|
+
@attributes = {}
|
49
|
+
return true if attributes_key_to_name_map.keys.empty?
|
50
|
+
results = Riaktor::Document::Attribute.find_many(
|
51
|
+
attributes_key_to_name_map.keys, :bucket => self.bucket)
|
52
|
+
results.each do |att_key, doc|
|
53
|
+
doc ||= Riaktor::Document::Attribute.new(:key => att_key, :bucket => self.bucket, :new => true)
|
54
|
+
doc.parent = self
|
55
|
+
name = attributes_key_to_name_map[att_key]
|
56
|
+
@attributes[name] = doc
|
57
|
+
end
|
58
|
+
@attributes
|
59
|
+
end # load_attributes
|
60
|
+
|
61
|
+
def save_attributes
|
62
|
+
return true unless @attributes
|
63
|
+
attributes_key_to_name_map.each do |att_key, att_name|
|
64
|
+
if attrib = @attributes[att_name]
|
65
|
+
attrib.save
|
66
|
+
end
|
67
|
+
end; true
|
68
|
+
end # save_attributes
|
69
|
+
|
70
|
+
def destroy_attributes
|
71
|
+
load_attributes
|
72
|
+
@attributes.each do |name, attrib|
|
73
|
+
attrib.destroy unless attrib.new_record?
|
74
|
+
end; true
|
75
|
+
end # destroy_attributes
|
76
|
+
end # InstanceMethods
|
77
|
+
end # Attributes
|
78
|
+
end # Document
|
79
|
+
end # Riaktor
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/attributes"
|
2
|
+
require "#{File.dirname(__FILE__)}/associations"
|
3
|
+
require "#{File.dirname(__FILE__)}/indexes"
|
4
|
+
|
5
|
+
module Riaktor
|
6
|
+
module Document
|
7
|
+
class Base < RObject
|
8
|
+
include Attributes
|
9
|
+
include Indexes
|
10
|
+
include Associations
|
11
|
+
include ActiveModel::Validations
|
12
|
+
extend ActiveModel::Callbacks
|
13
|
+
|
14
|
+
attr_accessor :links
|
15
|
+
|
16
|
+
define_model_callbacks :save, :destroy
|
17
|
+
|
18
|
+
validates_presence_of :key
|
19
|
+
|
20
|
+
def self.bucket
|
21
|
+
self.to_s.underscore
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find(key,opts={})
|
25
|
+
super(key,opts.merge(:bucket => self.bucket))
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find_many(keys,opts={})
|
29
|
+
super(keys,opts.merge(:bucket => self.bucket))
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.save_many(docs)
|
33
|
+
results = {}
|
34
|
+
docs.uniq.compact.in_groups_of(Riaktor::Client.max_concurrency) do |doc_group|
|
35
|
+
threads = []
|
36
|
+
doc_group.compact.each do |doc|
|
37
|
+
threads << Thread.new do
|
38
|
+
begin
|
39
|
+
results[doc] = doc.save
|
40
|
+
rescue Exception => e
|
41
|
+
results[doc] = e
|
42
|
+
end
|
43
|
+
end # Thread.new
|
44
|
+
end # doc_group.each
|
45
|
+
threads.each { |th| th.join }
|
46
|
+
end; results
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.destroy_many(docs)
|
50
|
+
results = {}
|
51
|
+
docs.uniq.compact.in_groups_of(Riaktor::Client.max_concurrency) do |doc_group|
|
52
|
+
threads = []
|
53
|
+
doc_group.compact.each do |doc|
|
54
|
+
threads << Thread.new do
|
55
|
+
begin
|
56
|
+
results[doc] = doc.destroy
|
57
|
+
rescue Exception => e
|
58
|
+
results[doc] = e
|
59
|
+
end
|
60
|
+
end # Thread.new
|
61
|
+
end # doc_group.each
|
62
|
+
threads.each { |th| th.join }
|
63
|
+
end; results
|
64
|
+
end
|
65
|
+
|
66
|
+
def bucket
|
67
|
+
self.class.bucket
|
68
|
+
end
|
69
|
+
|
70
|
+
def in_conflict?
|
71
|
+
super || any_attributes_in_conflict?
|
72
|
+
end
|
73
|
+
|
74
|
+
def changed?
|
75
|
+
super || any_associations_changed?
|
76
|
+
end
|
77
|
+
|
78
|
+
alias :ancestor_save :save
|
79
|
+
|
80
|
+
def save(opts={})
|
81
|
+
return false unless valid?
|
82
|
+
|
83
|
+
if new_record?
|
84
|
+
if existing = self.class.find(self.key)
|
85
|
+
errors[:key] << "is not unique"
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
_run_save_callbacks do
|
91
|
+
if self.links and !self.links.empty?
|
92
|
+
opts[:links] = self.links
|
93
|
+
end
|
94
|
+
|
95
|
+
if ancestor_save(opts)
|
96
|
+
save_attributes
|
97
|
+
save_indexes
|
98
|
+
end
|
99
|
+
end; true
|
100
|
+
end
|
101
|
+
|
102
|
+
alias :ancestor_destroy :destroy
|
103
|
+
|
104
|
+
def destroy(opts={})
|
105
|
+
return false if new_record?
|
106
|
+
_run_destroy_callbacks do
|
107
|
+
if ancestor_destroy(opts)
|
108
|
+
destroy_attributes
|
109
|
+
destroy_indexes
|
110
|
+
end
|
111
|
+
end; true
|
112
|
+
end
|
113
|
+
|
114
|
+
def read_attribute_for_validation(name)
|
115
|
+
case name.to_sym
|
116
|
+
when :key, :value
|
117
|
+
self.send(name.to_sym)
|
118
|
+
else
|
119
|
+
self.send(name.to_sym).value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end # Base
|
123
|
+
end # Document
|
124
|
+
end # Riaktor
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/index"
|
2
|
+
|
3
|
+
module Riaktor
|
4
|
+
module Document
|
5
|
+
module Indexes
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def defined_indexes
|
13
|
+
@defined_indexes || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def index(attrib_name, opts={})
|
17
|
+
@defined_indexes ||= {}
|
18
|
+
@defined_indexes[attrib_name] = opts
|
19
|
+
end
|
20
|
+
|
21
|
+
def key_for_index(name,value,opts={})
|
22
|
+
["indexes",name,Digest::SHA1.hexdigest(self.encode_value(value))].join("--")
|
23
|
+
end
|
24
|
+
|
25
|
+
def indexes_bucket
|
26
|
+
[self.bucket, "indexes"].join("--")
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_with_index(name, value, opts={})
|
30
|
+
key = key_for_index(name,value)
|
31
|
+
if index = Riaktor::Document::Index.find(key, {:bucket => indexes_bucket}.merge(opts))
|
32
|
+
self.find(index.value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end # ClassMethods
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
def indexes_key_to_name_map
|
39
|
+
@indexes_key_to_name_map ||= begin
|
40
|
+
map = {}
|
41
|
+
self.class.defined_indexes.each do |name, opts|
|
42
|
+
map[key_for_index(name,opts)] = name
|
43
|
+
end; map
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def save_indexes
|
48
|
+
load_attributes
|
49
|
+
Array(self.class.defined_indexes).in_groups_of(Riaktor::Client.max_concurrency).each do |defined_indexes_group|
|
50
|
+
threads = []
|
51
|
+
defined_indexes_group.each do |name_and_opts|
|
52
|
+
next unless name_and_opts
|
53
|
+
name = name_and_opts[0]
|
54
|
+
opts = name_and_opts[1]
|
55
|
+
attrib = @attributes[name]
|
56
|
+
next unless attrib.changed?
|
57
|
+
threads << Thread.new do
|
58
|
+
if !attrib.new_record? and attrib.orig_value
|
59
|
+
# delete old index
|
60
|
+
index_key = self.class.key_for_index(name,attrib.orig_value)
|
61
|
+
if index = Riaktor::Document::Index.find(index_key, :bucket => self.class.indexes_bucket)
|
62
|
+
index.parent = self
|
63
|
+
index.destroy
|
64
|
+
end
|
65
|
+
end
|
66
|
+
if attrib.value
|
67
|
+
# save new index
|
68
|
+
index_key = self.class.key_for_index(name,attrib.value)
|
69
|
+
index = Riaktor::Document::Index.new(:key => index_key, :bucket => self.class.indexes_bucket, :value => self.key)
|
70
|
+
index.parent = self
|
71
|
+
index.save
|
72
|
+
end
|
73
|
+
end # Thread.new do
|
74
|
+
end # defined_indexes_group.each
|
75
|
+
threads.each { |th| th.join }
|
76
|
+
end; true
|
77
|
+
end # save_indexes
|
78
|
+
|
79
|
+
def destroy_indexes
|
80
|
+
load_attributes
|
81
|
+
Array(self.class.defined_indexes).in_groups_of(Riaktor::Client.max_concurrency).each do |defined_indexes_group|
|
82
|
+
threads = []
|
83
|
+
defined_indexes_group.each do |name_and_opts|
|
84
|
+
next unless name_and_opts
|
85
|
+
name = name_and_opts[0]
|
86
|
+
opts = name_and_opts[1]
|
87
|
+
attrib = @attributes[name]
|
88
|
+
next unless attrib.orig_value
|
89
|
+
threads << Thread.new do
|
90
|
+
index_key = self.class.key_for_index(name,attrib.orig_value)
|
91
|
+
if index = Riaktor::Document::Index.find(index_key, :bucket => self.class.indexes_bucket)
|
92
|
+
index.parent = self
|
93
|
+
index.destroy
|
94
|
+
end
|
95
|
+
end # Thread.new do
|
96
|
+
end # defined_indexes_group.each
|
97
|
+
threads.each { |th| th.join }
|
98
|
+
end; true
|
99
|
+
end # destroy_indexes
|
100
|
+
|
101
|
+
end # InstanceMethods
|
102
|
+
end # Attributes
|
103
|
+
end # Document
|
104
|
+
end # Riaktor
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Riaktor
|
2
|
+
class RObject
|
3
|
+
class Link
|
4
|
+
attr_accessor :bucket, :key, :tag
|
5
|
+
|
6
|
+
def self.from_header(link_header)
|
7
|
+
return [] if link_header.nil?
|
8
|
+
links = []
|
9
|
+
link_header.split(",").each do |link_str|
|
10
|
+
url, tag = link_str.split(";")
|
11
|
+
url = url.gsub("<","").gsub(">","").strip.split("/")
|
12
|
+
bucket = url[-2]
|
13
|
+
key = url[-1]
|
14
|
+
tag = tag.split("=")[1].gsub("\"","").strip
|
15
|
+
next if tag == "up"
|
16
|
+
links << new(bucket,key,tag)
|
17
|
+
end; links
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.to_header(links)
|
21
|
+
links.reject! { |l| l.tag == "up" }
|
22
|
+
(links.collect { |l| l.to_s }).uniq.join(",")
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(bucket, key, tag)
|
26
|
+
@bucket = bucket
|
27
|
+
@key = key
|
28
|
+
@tag = tag
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(obj)
|
32
|
+
obj.is_a?(Link) && obj.bucket == self.bucket && obj.key == self.key && obj.tag == self.tag
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"</riak/#{bucket}/#{key}>; riaktag=\"#{tag}\""
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_doc(klass)
|
40
|
+
klass.find(key, :bucket => bucket)
|
41
|
+
end
|
42
|
+
end # Link
|
43
|
+
end # RObject
|
44
|
+
end # Riaktor
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/link"
|
2
|
+
|
3
|
+
module Riaktor
|
4
|
+
class RObject
|
5
|
+
module Persistence
|
6
|
+
class SaveError < RuntimeError; end
|
7
|
+
class DestroyError < RuntimeError; end
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.send :extend, ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def find(key,opts={})
|
16
|
+
find_many([key],opts)[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_many(keys,opts={})
|
20
|
+
raise(ArgumentError, "missing :bucket") unless opts[:bucket]
|
21
|
+
raise(ArgumentError, "empty keys") if keys.nil? or keys.empty?
|
22
|
+
|
23
|
+
operations = []
|
24
|
+
keys.each do |key|
|
25
|
+
op = {}
|
26
|
+
op["bucket"] = opts[:bucket]
|
27
|
+
op["key"] = key
|
28
|
+
op["params"] = { "r" => opts["r"] || 2 }
|
29
|
+
operations << op
|
30
|
+
end # keys.each
|
31
|
+
|
32
|
+
found = {}
|
33
|
+
Riaktor::Client.get_many(operations).each do |key_bucket_pair, resp|
|
34
|
+
bucket = key_bucket_pair[0]
|
35
|
+
key = key_bucket_pair[1]
|
36
|
+
case resp["code"]
|
37
|
+
when 200 then
|
38
|
+
found[key] = new(:key => key,
|
39
|
+
:bucket => bucket,
|
40
|
+
:value => decode_value(resp["body"]),
|
41
|
+
:vector_clock => resp["headers"]["X-Riak-Vclock"],
|
42
|
+
:links => Link.from_header(resp["headers"]["Link"]),
|
43
|
+
:new => false)
|
44
|
+
when 300 then
|
45
|
+
found[key] = new(:key => key,
|
46
|
+
:bucket => bucket,
|
47
|
+
:value => nil,
|
48
|
+
:vector_clock => resp["headers"]["X-Riak-Vclock"],
|
49
|
+
:links => Link.from_header(resp["headers"]["Link"]),
|
50
|
+
:new => false)
|
51
|
+
vtags = resp["body"].split("\n")[1..-1]
|
52
|
+
found[key].load_siblings(vtags,{"r" => opts["r"] || 2})
|
53
|
+
else
|
54
|
+
found[key] = nil
|
55
|
+
end
|
56
|
+
end # Riaktor.get_many
|
57
|
+
|
58
|
+
found
|
59
|
+
end
|
60
|
+
|
61
|
+
def decode_value(val)
|
62
|
+
case self.encoder
|
63
|
+
when :json
|
64
|
+
Yajl::Parser.parse(val)
|
65
|
+
when :yaml
|
66
|
+
YAML.load(val)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def encode_value(val)
|
71
|
+
case self.encoder
|
72
|
+
when :json
|
73
|
+
Yajl::Encoder.encode(val)
|
74
|
+
when :yaml
|
75
|
+
YAML.dump(val)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end # ClassMethods
|
79
|
+
|
80
|
+
module InstanceMethods
|
81
|
+
def load_siblings(vtags,params={})
|
82
|
+
params["r"] ||= 2
|
83
|
+
@siblings = {}
|
84
|
+
|
85
|
+
vtags.each do |vtag|
|
86
|
+
resp = Riaktor::Client.get({ "bucket" => self.bucket, "key" => self.key,
|
87
|
+
"params" => { "vtag" => vtag }.merge(params) })
|
88
|
+
if resp["code"] == 200
|
89
|
+
@siblings[vtag] = self.class.decode_value(resp["body"])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@siblings
|
94
|
+
end
|
95
|
+
|
96
|
+
def resolved!
|
97
|
+
@siblings = nil; true
|
98
|
+
end
|
99
|
+
|
100
|
+
def in_conflict?
|
101
|
+
!@siblings.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
def save(opts={})
|
105
|
+
raise(SaveError, "cannot save with siblings present") if self.in_conflict?
|
106
|
+
if changed? or opts[:force] == true
|
107
|
+
op = {}
|
108
|
+
op["bucket"] = self.bucket
|
109
|
+
op["key"] = self.key
|
110
|
+
op["value"] = self.class.encode_value(self.value)
|
111
|
+
op["params"] = { "dw" => opts[:dw] || 0, "w" => opts[:w] || 2, "r" => opts[:r] || 2 }
|
112
|
+
op["headers"] = { "X-Riak-Vclock" => self.vector_clock } if self.vector_clock
|
113
|
+
op["headers"]["Link"] = Link.to_header(opts[:links]) if opts[:links]
|
114
|
+
resp = Riaktor::Client.put(op)
|
115
|
+
if [204,404].include?(resp["code"])
|
116
|
+
resp = Riaktor::Client.get(op)
|
117
|
+
end
|
118
|
+
case resp["code"]
|
119
|
+
when 200 then
|
120
|
+
@links = Link.from_header(resp["headers"]["Link"])
|
121
|
+
@vector_clock = resp["headers"]["X-Riak-Vclock"]
|
122
|
+
@new = false
|
123
|
+
when 300 then
|
124
|
+
@links = Link.from_header(resp["headers"]["Link"])
|
125
|
+
@vector_clock = resp["headers"]["X-Riak-Vclock"]
|
126
|
+
@new = false
|
127
|
+
vtags = resp["body"].split("\n")[1..-1]
|
128
|
+
load_siblings(vtags,{"r" => op["params"]["r"]})
|
129
|
+
else
|
130
|
+
raise(SaveError, resp["code"])
|
131
|
+
end # case
|
132
|
+
end
|
133
|
+
true
|
134
|
+
end
|
135
|
+
|
136
|
+
def destroy(opts={})
|
137
|
+
raise(DestroyError, "cannot destroy with siblings present") if self.in_conflict?
|
138
|
+
raise(DestroyError, "cannot destroy unsaved document") if self.new_record?
|
139
|
+
op = {}
|
140
|
+
op["bucket"] = self.bucket
|
141
|
+
op["key"] = self.key
|
142
|
+
op["params"] = { "dw" => opts[:dw] || 0, "w" => opts[:w] || 2, "r" => opts[:r] || 2 }
|
143
|
+
op["headers"] = { "X-Riak-Vclock" => self.vector_clock } if self.vector_clock
|
144
|
+
resp = Riaktor::Client.delete(op)
|
145
|
+
case resp["code"]
|
146
|
+
when 204 then
|
147
|
+
@deleted = true
|
148
|
+
else
|
149
|
+
raise(DestroyError, resp["code"])
|
150
|
+
end # case
|
151
|
+
end
|
152
|
+
end # InstanceMethods
|
153
|
+
end # Persistence
|
154
|
+
end # Document
|
155
|
+
end # Riaktor
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/r_object/persistence"
|
2
|
+
|
3
|
+
module Riaktor
|
4
|
+
class RObject
|
5
|
+
include Persistence
|
6
|
+
|
7
|
+
attr_accessor :key, :bucket, :value, :orig_value, :links, :siblings, :vector_clock, :deleted
|
8
|
+
|
9
|
+
def self.encoder
|
10
|
+
@encoder || :yaml
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.encoder=(val)
|
14
|
+
@encoder = val
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(opts={})
|
18
|
+
@key = opts[:key]
|
19
|
+
@value = opts[:value]
|
20
|
+
@bucket = opts[:bucket]
|
21
|
+
@siblings = opts[:siblings]
|
22
|
+
@links = opts[:links] || []
|
23
|
+
@new = opts[:new].nil? ? true : opts[:new]
|
24
|
+
@orig_value = @new == true ? nil : @value.dup rescue nil
|
25
|
+
@vector_clock = opts[:vector_clock]
|
26
|
+
end
|
27
|
+
|
28
|
+
alias :id :key
|
29
|
+
|
30
|
+
def ==(obj)
|
31
|
+
self.class.to_s == obj.class.to_s && self.key == obj.key
|
32
|
+
end
|
33
|
+
|
34
|
+
def changed?
|
35
|
+
@value != @orig_value
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_record?
|
39
|
+
@new == true
|
40
|
+
end
|
41
|
+
|
42
|
+
def destroyed?
|
43
|
+
@deleted == true
|
44
|
+
end
|
45
|
+
end # RObject
|
46
|
+
end # Riaktor
|
data/lib/riaktor.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ben Myles
|
@@ -146,11 +146,23 @@ extra_rdoc_files:
|
|
146
146
|
- PostInstall.txt
|
147
147
|
files:
|
148
148
|
- History.txt
|
149
|
+
- lib/riaktor/client.rb
|
150
|
+
- lib/riaktor/document/association.rb
|
151
|
+
- lib/riaktor/document/associations.rb
|
152
|
+
- lib/riaktor/document/attribute.rb
|
153
|
+
- lib/riaktor/document/attributes.rb
|
154
|
+
- lib/riaktor/document/base.rb
|
155
|
+
- lib/riaktor/document/index.rb
|
156
|
+
- lib/riaktor/document/indexes.rb
|
157
|
+
- lib/riaktor/document.rb
|
158
|
+
- lib/riaktor/r_object/link.rb
|
159
|
+
- lib/riaktor/r_object/persistence.rb
|
160
|
+
- lib/riaktor/r_object.rb
|
161
|
+
- lib/riaktor.rb
|
149
162
|
- Manifest.txt
|
150
163
|
- PostInstall.txt
|
151
|
-
- README.rdoc
|
152
164
|
- Rakefile
|
153
|
-
-
|
165
|
+
- README.rdoc
|
154
166
|
- script/console
|
155
167
|
- script/destroy
|
156
168
|
- script/generate
|