liveresource 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +1 -0
  2. data/BSDL +24 -0
  3. data/COPYING +59 -0
  4. data/GPL +339 -0
  5. data/README.md +289 -0
  6. data/Rakefile +47 -0
  7. data/benchmark/benchmark_helper.rb +19 -0
  8. data/benchmark/method_benchmark.rb +94 -0
  9. data/lib/live_resource.rb +66 -0
  10. data/lib/live_resource/attributes.rb +77 -0
  11. data/lib/live_resource/declarations.rb +200 -0
  12. data/lib/live_resource/finders.rb +43 -0
  13. data/lib/live_resource/log_helper.rb +24 -0
  14. data/lib/live_resource/methods.rb +41 -0
  15. data/lib/live_resource/methods/dispatcher.rb +176 -0
  16. data/lib/live_resource/methods/forward.rb +23 -0
  17. data/lib/live_resource/methods/future.rb +27 -0
  18. data/lib/live_resource/methods/method.rb +93 -0
  19. data/lib/live_resource/methods/token.rb +22 -0
  20. data/lib/live_resource/redis_client.rb +100 -0
  21. data/lib/live_resource/redis_client/attributes.rb +40 -0
  22. data/lib/live_resource/redis_client/methods.rb +194 -0
  23. data/lib/live_resource/redis_client/registration.rb +25 -0
  24. data/lib/live_resource/resource.rb +44 -0
  25. data/lib/live_resource/resource_proxy.rb +180 -0
  26. data/old/benchmark/attribute_benchmark.rb +58 -0
  27. data/old/benchmark/thread_benchmark.rb +89 -0
  28. data/old/examples/attribute.rb +22 -0
  29. data/old/examples/attribute_rmw.rb +30 -0
  30. data/old/examples/attribute_subscriber.rb +32 -0
  31. data/old/examples/method_provider_sleep.rb +22 -0
  32. data/old/examples/methods.rb +37 -0
  33. data/old/lib/live_resource/subscriber.rb +98 -0
  34. data/old/redis_test.rb +127 -0
  35. data/old/state_publisher_test.rb +139 -0
  36. data/old/test/attribute_modify_test.rb +52 -0
  37. data/old/test/attribute_options_test.rb +54 -0
  38. data/old/test/attribute_subscriber_test.rb +94 -0
  39. data/old/test/composite_resource_test.rb +61 -0
  40. data/old/test/method_sender_test.rb +41 -0
  41. data/old/test/redis_api_test.rb +185 -0
  42. data/old/test/simple_attribute_test.rb +75 -0
  43. data/test/attribute_test.rb +212 -0
  44. data/test/declarations_test.rb +119 -0
  45. data/test/logger_test.rb +44 -0
  46. data/test/method_call_test.rb +223 -0
  47. data/test/method_forward_continue_test.rb +83 -0
  48. data/test/method_params_test.rb +81 -0
  49. data/test/method_routing_test.rb +59 -0
  50. data/test/multiple_class_test.rb +47 -0
  51. data/test/new_api_DISABLED.rb +127 -0
  52. data/test/test_helper.rb +9 -0
  53. data/test/volume_create_DISABLED.rb +74 -0
  54. metadata +129 -0
@@ -0,0 +1,100 @@
1
+ require 'rubygems'
2
+ require 'redis'
3
+ require 'yaml'
4
+ require_relative 'log_helper'
5
+ require_relative 'redis_client/attributes'
6
+ require_relative 'redis_client/methods'
7
+ require_relative 'redis_client/registration'
8
+
9
+ class Redis
10
+ def clone
11
+ # Create independent Redis
12
+ Redis.new(
13
+ :host => client.host,
14
+ :port => client.port,
15
+ :timeout => client.timeout,
16
+ :logger => client.logger,
17
+ :password => client.password,
18
+ :db => client.db)
19
+ end
20
+ end
21
+
22
+ module LiveResource
23
+ class RedisClient
24
+ include LogHelper
25
+ attr_writer :redis
26
+ attr_reader :redis_class, :redis_name
27
+
28
+ @@logger = Logger.new(STDERR)
29
+ @@logger.level = Logger::WARN
30
+
31
+ def initialize(resource_class, resource_name)
32
+ @redis_class = RedisClient.redisized_key(resource_class)
33
+ @redis_name = RedisClient.redisized_key(resource_name)
34
+
35
+ self.logger = self.class.logger
36
+ end
37
+
38
+ def method_missing(method, *params, &block)
39
+ if self.class.redis.respond_to? method
40
+ redis_command(method, params, &block)
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def respond_to?(method)
47
+ return true if self.class.redis.respond_to?(method)
48
+ super
49
+ end
50
+
51
+ # Override default (Ruby) exec with Redis exec.
52
+ def exec
53
+ redis_command(:exec, nil)
54
+ end
55
+
56
+ def self.redis
57
+ # Hash of Thread -> Redis instances
58
+ @@redis ||= {}
59
+ @@proto_redis ||= Redis.new
60
+
61
+ if @@redis[Thread.current].nil?
62
+ @@redis[Thread.current] = @@proto_redis.clone
63
+ end
64
+
65
+ @@redis[Thread.current]
66
+ end
67
+
68
+ def self.redis=(redis)
69
+ @@proto_redis = redis
70
+ @@redis = {}
71
+ end
72
+
73
+ def self.logger
74
+ @@logger
75
+ end
76
+
77
+ def self.logger=(logger)
78
+ @@logger = logger
79
+ end
80
+
81
+ def self.redisized_key(word)
82
+ word = word.to_s.dup
83
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
84
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
85
+ word.tr!("-", "_")
86
+ word.gsub!('::', '-')
87
+ word.downcase!
88
+ word
89
+ end
90
+
91
+ private
92
+
93
+ def redis_command(method, params, &block)
94
+ debug ">>", method.to_s, *params
95
+ response = self.class.redis.send(method, *params, &block)
96
+ debug "<<", response
97
+ response
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,40 @@
1
+ module LiveResource
2
+ class RedisClient
3
+ def remote_attributes_key
4
+ if @redis_class == "class"
5
+ "#{@redis_name}.class_attributes"
6
+ else
7
+ "#{@redis_class}.attributes"
8
+ end
9
+ end
10
+
11
+ def register_attributes(attributes)
12
+ unless attributes.empty?
13
+ sadd remote_attributes_key, attributes
14
+ end
15
+ end
16
+
17
+ def registered_attributes
18
+ attributes = smembers remote_attributes_key
19
+
20
+ attributes.map { |a| a.to_sym }
21
+ end
22
+
23
+ def attribute_read(key, options={})
24
+ deserialize(get("#{@redis_class}.#{@redis_name}.attributes.#{key}"))
25
+ end
26
+
27
+ def attribute_write(key, value, options={})
28
+ redis_key = "#{@redis_class}.#{@redis_name}.attributes.#{key}"
29
+ if options[:no_overwrite]
30
+ setnx(redis_key, serialize(value))
31
+ else
32
+ set(redis_key, serialize(value))
33
+ end
34
+ end
35
+
36
+ def attribute_watch(key)
37
+ watch("#{@redis_class}.#{redis_name}.attributes.#{key}")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,194 @@
1
+ require 'yaml'
2
+
3
+ module LiveResource
4
+ class RedisClient
5
+ def remote_methods_key
6
+ if @redis_class == "class"
7
+ "#{@redis_name}.class_methods"
8
+ else
9
+ "#{@redis_class}.methods"
10
+ end
11
+ end
12
+
13
+ def methods_list
14
+ "#{@redis_class}.#{@redis_name}.methods_pending"
15
+ end
16
+
17
+ def methods_in_progress_list
18
+ "#{@redis_class}.#{@redis_name}.methods_in_progress"
19
+ end
20
+
21
+ def method_details(token)
22
+ "#{token.redis_class}.#{token.redis_name}.method.#{token.seq}"
23
+ end
24
+
25
+ def result_details(token)
26
+ "#{token.redis_class}.#{token.redis_name}.result.#{token.seq}"
27
+ end
28
+
29
+ def register_methods(methods)
30
+ unless methods.empty?
31
+ sadd remote_methods_key, methods
32
+ end
33
+ end
34
+
35
+ def registered_methods
36
+ methods = smembers remote_methods_key
37
+
38
+ methods.map { |m| m.to_sym }
39
+ end
40
+
41
+ def method_wait
42
+ token = brpoplpush methods_list, methods_in_progress_list, 0
43
+ deserialize(token)
44
+ end
45
+
46
+ def method_push(token)
47
+ lpush methods_list, serialize(token)
48
+ end
49
+
50
+ def method_done(token)
51
+ lrem methods_in_progress_list, 0, serialize(token)
52
+ end
53
+
54
+ def method_get(token)
55
+ method = get method_details(token)
56
+
57
+ deserialize(method)
58
+ end
59
+
60
+ def method_send(method)
61
+ unless method.token
62
+ # Choose unique token for this action; retry if token is already in
63
+ # use by another action.
64
+ loop do
65
+ method.token = RemoteMethodToken.new(
66
+ @redis_class,
67
+ @redis_name,
68
+ sprintf("%05d", Kernel.rand(100000)))
69
+
70
+ break if setnx(method_details(method.token), serialize(method))
71
+ end
72
+ else
73
+ # Re-serialize current state of method to existing location.
74
+ set method_details(method.token), serialize(method)
75
+ end
76
+
77
+ method_push method.token
78
+ method
79
+ end
80
+
81
+ def method_result(method, result)
82
+ token = method.token
83
+
84
+ # Need to watch the method while setting the result; if the caller
85
+ # has given up waiting before we set the result, we don't want to
86
+ # leave extra crud in Redis.
87
+ watch method_details(token)
88
+
89
+ unless exists(method_details(token))
90
+ # Caller must have deleted method
91
+ warn "setting result for method #{token}, but caller deleted it"
92
+ unwatch
93
+ return
94
+ end
95
+
96
+ begin
97
+ multi
98
+ lpush result_details(token), serialize(result)
99
+ exec
100
+ rescue RuntimeError => e
101
+ # Must have been deleted while we were working on it, bail out.
102
+ warn e
103
+ discard
104
+ end
105
+ end
106
+
107
+ def method_wait_for_result(method, timeout)
108
+ token = method.token
109
+ result = nil
110
+
111
+ begin
112
+ list, result = brpop result_details(token), timeout
113
+
114
+ if result.nil?
115
+ raise RuntimeError.new("timed out waiting for method #{token}")
116
+ end
117
+
118
+ result = deserialize(result)
119
+ rescue
120
+ # Clean token from any lists before passing up exception
121
+ method_cleanup(token)
122
+ raise
123
+ ensure
124
+ # Clear out original method call details
125
+ del method_details(token)
126
+ end
127
+ end
128
+
129
+ def method_discard_result(token)
130
+ del result_details(token)
131
+ del method_details(token)
132
+ end
133
+
134
+ def method_done_with?(method)
135
+ st = serialize(method.token)
136
+
137
+ # Need to do a multi/exec so we can atomically look in 3 lists
138
+ # for the token
139
+ multi
140
+ lrange methods_list, 0, -1
141
+ lrange methods_in_progress_list, 0, -1
142
+ lrange result_details(method.token), 0, -1
143
+ result = exec
144
+
145
+ if (result[2] != [])
146
+ # Result already pending
147
+ true
148
+ elsif result[0].include?(st) or result[1].include?(st)
149
+ # Still in methods or methods-in-progress
150
+ false
151
+ else
152
+ raise ArgumentError.new("No method #{token} pending")
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def method_cleanup(token)
159
+ st = serialize(token)
160
+
161
+ # Need to do a multi/exec so we can atomically delete from all 3 lists
162
+ multi
163
+ lrem methods_list, 0, st
164
+ lrem methods_in_progress_list, 0, st
165
+ lrem result_details(token), 0, st
166
+ exec
167
+ end
168
+
169
+ def serialize(value)
170
+ if value.is_a? Exception
171
+ # YAML can't dump an exception properly, it loses the message.
172
+ # and the backtrace. Save those separately as strings.
173
+ YAML::dump [value, value.message, value.backtrace]
174
+ else
175
+ YAML::dump value
176
+ end
177
+ end
178
+
179
+ def deserialize(value)
180
+ return nil if value.nil?
181
+
182
+ result = YAML::load(value)
183
+
184
+ if result.is_a?(Array) and result[0].is_a?(Exception)
185
+ # Inverse of what serialize() is doing with exceptions.
186
+ e = result[0].class.new(result[1])
187
+ e.set_backtrace result[2]
188
+ result = e
189
+ end
190
+
191
+ result
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,25 @@
1
+ module LiveResource
2
+ class RedisClient
3
+ def instances_key
4
+ "#{@redis_class}.instances"
5
+ end
6
+
7
+ def register
8
+ hincrby instances_key, @redis_name, 1
9
+ end
10
+
11
+ def unregister
12
+ hincrby instances_key, @redis_name, -1
13
+ end
14
+
15
+ def all
16
+ names = []
17
+
18
+ hgetall(instances_key).each_pair do |i, count|
19
+ names << i if (count.to_i > 0)
20
+ end
21
+
22
+ names
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'log_helper'
2
+ require_relative 'declarations'
3
+ require_relative 'finders'
4
+ require_relative 'attributes'
5
+ require_relative 'methods'
6
+ require_relative 'methods/forward'
7
+
8
+ module LiveResource
9
+
10
+ # Module for all Resource providers. Any instances of resources should
11
+ # be registered with LiveResource::register. The class may also be
12
+ # registered, if any class attributes/methods should be remotely
13
+ # callable.
14
+ module Resource
15
+ include LiveResource::LogHelper
16
+ include LiveResource::Declarations
17
+ include LiveResource::Finders
18
+ include LiveResource::Attributes
19
+ include LiveResource::Methods
20
+
21
+ # Extends resource classes with proper class methods and
22
+ # class-level method dispatcher.
23
+ def self.included(base)
24
+ base.extend(LiveResource::Declarations::ClassMethods)
25
+
26
+ # The class is also extended with attribute and method support
27
+ # (i.e, the method dispatcher).
28
+ base.extend(LiveResource::Attributes)
29
+ base.extend(LiveResource::Methods)
30
+ end
31
+
32
+ # Create forward instruction that can be returned by a remote
33
+ # method, instructing LiveResource to forward to a different
34
+ # remote method instead of returing directly to the caller.
35
+ #
36
+ # @param [LiveResource::ResourceProxy] resource the resource to forward to
37
+ # @param [Symbol] method the resource's method to call
38
+ # @param params any parameters to pass with the method call
39
+ # @return [LiveResource::RemoteMethodForward] a forward instruction, used internally by LiveResource
40
+ def forward(resource, method, *params)
41
+ LiveResource::RemoteMethodForward.new(resource, method, params)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,180 @@
1
+ require_relative 'log_helper'
2
+ require_relative 'redis_client'
3
+ require_relative 'methods/method'
4
+ require_relative 'methods/future'
5
+
6
+ module LiveResource
7
+
8
+ # Client object that represents a resource, allowing method calls
9
+ # and getting/setting attributes. Typically these are returned from
10
+ # LiveResource finder methods (all, find, etc).
11
+ class ResourceProxy
12
+ include LiveResource::LogHelper
13
+
14
+ attr_reader :redis_class, :redis_name
15
+
16
+ # Create a new proxy given its Redis class and name; typically NOT
17
+ # USED by client code -- use methods of LiveResource::Finders
18
+ # instead.
19
+ def initialize(redis_class, redis_name)
20
+ @redis_class = redis_class
21
+ @redis_name = redis_name
22
+ @redis = RedisClient.new(redis_class, redis_name)
23
+ @remote_methods = @redis.registered_methods
24
+ @remote_attributes = @redis.registered_attributes
25
+ end
26
+
27
+ # Proxies attribute and remote method calls to the back-end provider.
28
+ def method_missing(m, *params, &block)
29
+ # Strip trailing ?, ! for seeing if we support method
30
+ sm = m.to_s.sub(/[!,?]$/, '').to_sym
31
+
32
+ if @remote_attributes.include?(m)
33
+ # Attribute get/set
34
+ if m.match(/\=$/)
35
+ m = m.to_s.sub(/\=$/, '').to_sym # Strip trailing equal
36
+
37
+ remote_attribute_write(m, *params)
38
+ else
39
+ remote_attribute_read(m)
40
+ end
41
+ elsif @remote_methods.include?(sm)
42
+ # Method call
43
+ method = RemoteMethod.new(
44
+ :method => sm,
45
+ :params => params)
46
+
47
+ if m.match(/!$/)
48
+ # Async call, discard result
49
+ method.flags[:discard_result] = true
50
+
51
+ remote_send method
52
+ elsif m.match(/\?$/)
53
+ # Async call with future
54
+ method = remote_send method
55
+ Future.new(self, method)
56
+ else
57
+ # Synchronous method call
58
+ wait_for_done remote_send(method)
59
+ end
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ # Checks if method is a supported attribute or remote method.
66
+ #
67
+ # @param [LiveResource::RemoteMethod] method method to send
68
+ # @param [Object] include_private unused
69
+ def respond_to_missing?(method, include_private)
70
+ stripped_method = method.to_s.sub(/[!,?]$/, '').to_sym
71
+
72
+ @remote_methods.include?(stripped_method) or
73
+ @remote_attributes.include?(method)
74
+ end
75
+
76
+ # Send a already-created method object; not typically used by
77
+ # clients -- use method_missing interface instead.
78
+ #
79
+ # @param [LiveResource::RemoteMethod] method method to send
80
+ def remote_send(method)
81
+ @redis.method_send method
82
+ end
83
+
84
+ # Wait for method to finish, blocks if method not complete. An
85
+ # exception raised by the remote resource will be captured and
86
+ # raised in the client's thread. Clients may only wait once for
87
+ # completion.
88
+ #
89
+ # @param [LiveResource::RemoteMethod] method method to wait for
90
+ # @param [Numeric] timeout seconds to wait for method completion
91
+ def wait_for_done(method, timeout = 0)
92
+ result = @redis.method_wait_for_result(method, timeout)
93
+
94
+ if result.is_a?(Exception)
95
+ # Merge the backtrace from the passed exception with this
96
+ # stack trace so the final backtrace looks like the method_sender
97
+ # called the method_provider directly.
98
+ # trace = merge_backtrace caller, result.backtrace
99
+ # result.set_backtrace trace
100
+
101
+ result.set_backtrace result.backtrace
102
+ raise result
103
+ else
104
+ result
105
+ end
106
+ end
107
+
108
+ # Check if remote method is already complete. May be called multiple times.
109
+ #
110
+ # @param [LiveResource::RemoteMethod] method method to check on
111
+ def done_with?(method)
112
+ @redis.method_done_with? method
113
+ end
114
+
115
+ # Reads remote attribute.
116
+ #
117
+ # @param [Symbol] key attribute name
118
+ # @return [Object] remote attribute value
119
+ def remote_attribute_read(key, options = {})
120
+ @redis.attribute_read(key, options)
121
+ end
122
+
123
+ # Writes remote attribute to new value.
124
+ #
125
+ # @param [Symbol] key attribute name
126
+ # @param [Object] value new value for attribute
127
+ # @return new value for attribute
128
+ def remote_attribute_write(key, value, options = {})
129
+ @redis.attribute_write(key, value, options)
130
+ end
131
+
132
+ def inspect
133
+ "#{self.class}: #{@redis_class} #{@redis_name}"
134
+ end
135
+
136
+ # Specify custom format when YAML encoding
137
+ def encode_with coder
138
+ coder.tag = '!live_resource:resource'
139
+ coder['class'] = @redis_class
140
+ coder['name'] = @redis_name
141
+ end
142
+
143
+ private
144
+
145
+ # Merge the stack trace from the method sendor and method
146
+ # provider so it looks like one, seamless stack trace.
147
+ # LiveResource traces are removed and replaced with a simple
148
+ # 'via LiveResource' type message.
149
+ def merge_backtrace(sender_trace, provider_trace)
150
+ return nil if provider_trace.nil?
151
+ return provider_trace if sender_trace.nil?
152
+
153
+ # Find the first live resource stack trace
154
+ index = provider_trace.index do |t|
155
+ t =~ /lib\/live_resource\/method_provider/ ## FIXME
156
+ end
157
+
158
+ # Slice off everything starting at that index
159
+ result = provider_trace[0 .. (index - 1)]
160
+
161
+ # Add a trace that indicates that live resource was used
162
+ # to link the sender to the provider.
163
+ result << 'via LiveResource'
164
+
165
+ # For the sender trace, remove the 'method_sendor'
166
+ # part of the trace.
167
+ index = sender_trace.index do |t|
168
+ t =~ /lib\/live_resource\/method_sender/ ## FIXME
169
+ end
170
+ result += sender_trace[(index + 1) .. (sender_trace.length - 1)]
171
+
172
+ result
173
+ end
174
+ end
175
+ end
176
+
177
+ # Make YAML parser create ResourceProxy objects from our custom type.
178
+ Psych.add_domain_type('live_resource', 'resource') do |type, val|
179
+ LiveResource::ResourceProxy.new(val['class'], val['name'])
180
+ end