liveresource 2.0.0

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.
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