riaktor 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|