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,43 @@
1
+ require_relative 'resource_proxy'
2
+
3
+ module LiveResource
4
+ module Finders
5
+
6
+ def LiveResource.all(resource_class)
7
+ redis_names = RedisClient.new(resource_class, nil).all
8
+
9
+ redis_names.map do |redis_name|
10
+ ResourceProxy.new(RedisClient.redisized_key(resource_class), redis_name)
11
+ end
12
+ end
13
+
14
+ def LiveResource.find(resource_class, resource_name = nil, &block)
15
+ if resource_name.nil? and block.nil?
16
+ # Find class resource instead of instance resource.
17
+ resource_name = resource_class
18
+ resource_class = "class"
19
+ end
20
+
21
+ if block.nil?
22
+ block = lambda { |name| name == RedisClient.redisized_key(resource_name) ? name : nil }
23
+ end
24
+
25
+ redis_name = RedisClient.new(resource_class, nil).all.find do |name|
26
+ block.call(name)
27
+ end
28
+
29
+ if redis_name
30
+ ResourceProxy.new(RedisClient.redisized_key(resource_class), redis_name)
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ def LiveResource.any(resource_class)
37
+ resources = all(resource_class)
38
+
39
+ resources[rand(resources.length)]
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,24 @@
1
+ require 'logger'
2
+
3
+ module LiveResource
4
+ module LogHelper
5
+ def logger
6
+ if @logger.nil?
7
+ @logger = Logger.new(STDERR)
8
+ @logger.level = Logger::WARN
9
+ end
10
+
11
+ @logger
12
+ end
13
+
14
+ def logger=(logger)
15
+ @logger = logger
16
+ end
17
+
18
+ [:debug, :info, :warn, :error, :fatal].each do |level|
19
+ define_method(level) do |*params|
20
+ logger.send(level, params.join(' '))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'methods/dispatcher'
2
+
3
+ module LiveResource
4
+ module Methods
5
+ attr_reader :dispatcher
6
+
7
+ # Start the method dispatcher for this resource. On return, the
8
+ # resource will be visible to finders (.all(), etc.)
9
+ # and remote methods may be called.
10
+ def start
11
+ if @dispatcher
12
+ @dispatcher.start
13
+ else
14
+ @dispatcher = RemoteMethodDispatcher.new(self)
15
+ end
16
+
17
+ @dispatcher.wait_for_running
18
+ self
19
+ end
20
+
21
+ def stop
22
+ return if @dispatcher.nil?
23
+
24
+ @dispatcher.stop
25
+ self
26
+ end
27
+
28
+ def running?
29
+ @dispatcher && @dispatcher.running?
30
+ end
31
+
32
+ def remote_methods
33
+ if self.is_a? Class
34
+ remote_singleton_methods
35
+ else
36
+ self.class.remote_instance_methods
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,176 @@
1
+ require_relative '../log_helper'
2
+ require_relative '../redis_client'
3
+
4
+ module LiveResource
5
+ class RemoteMethodDispatcher
6
+ include LogHelper
7
+
8
+ attr_reader :thread, :resource
9
+
10
+ def initialize(resource)
11
+ @resource = resource
12
+ @thread = nil
13
+ @running = false
14
+
15
+ start
16
+ end
17
+
18
+ def redis
19
+ @resource.redis
20
+ end
21
+
22
+ def start
23
+ return if @thread
24
+
25
+ @thread = Thread.new { run }
26
+ end
27
+
28
+ def stop
29
+ return if @thread.nil?
30
+
31
+ redis.method_push exit_token
32
+ @running = false
33
+ @thread.join
34
+ @thread = nil
35
+ end
36
+
37
+ def running?
38
+ (@thread != nil) && @running
39
+ end
40
+
41
+ def wait_for_running
42
+ while !running? do
43
+ Thread.pass
44
+ end
45
+ end
46
+
47
+ def run
48
+ info("#{self} method dispatcher starting")
49
+
50
+ # Register methods and attributes used by this resource class
51
+ redis.register_methods @resource.remote_methods
52
+ redis.register_attributes @resource.remote_attributes
53
+
54
+ # Need to register our class and instance in Redis so the finders
55
+ # (all, any, etc.) will work.
56
+ redis.register
57
+
58
+ @running = true
59
+
60
+ begin
61
+ loop do
62
+ token = redis.method_wait
63
+
64
+ if is_exit_token(token)
65
+ if token == exit_token
66
+ redis.method_done token
67
+ break
68
+ else
69
+ redis.method_push token
70
+ next
71
+ end
72
+ end
73
+
74
+ method = redis.method_get(token)
75
+
76
+ begin
77
+ result = validate_method(method).call(*method.params)
78
+
79
+ if result.is_a? Resource
80
+ # Return descriptor of a resource proxy instead
81
+ result = ResourceProxy.new(
82
+ result.redis.redis_class,
83
+ result.redis.redis_name)
84
+ elsif result.is_a? RemoteMethodForward
85
+ # Append forwarding instructions to current method
86
+ method.forward_to result
87
+ end
88
+
89
+ if method.final_destination?
90
+ redis.method_result method, result
91
+ else
92
+ # Forward on to next step in method's path
93
+ dest = method.next_destination!
94
+
95
+ unless result.is_a? RemoteMethodForward
96
+ # First parameter(s) to next method will be the result
97
+ # of this method call.
98
+ if result.is_a? Array
99
+ method.params = result + method.params
100
+ else
101
+ method.params.unshift result
102
+ end
103
+ end
104
+
105
+ dest.remote_send method
106
+ end
107
+ rescue Exception => e
108
+ # TODO: custom encoding for exception to make it less
109
+ # Ruby-specific.
110
+
111
+ debug "Method #{method.token} failed:", e.message
112
+ redis.method_result method, e
113
+ end
114
+
115
+ redis.method_done token
116
+ redis.method_discard_result(token) if method.flags[:discard_result]
117
+ end
118
+ ensure
119
+ # NOTE: if this process crashes outright, or we lose network
120
+ # connection to Redis, or whatever -- this decrement won't occur.
121
+ # Supervisor should clean up where possible.
122
+ redis.unregister
123
+
124
+ info("#{self} method dispatcher exiting")
125
+
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ # Verify validity of remote method being called
132
+ def validate_method(m)
133
+
134
+ # Check that method is remote callable
135
+ unless @resource.remote_methods.include?(m.method)
136
+ raise NoMethodError.new("Undefined method `#{m.method}' (#{@resource.remote_methods.join(', ')})")
137
+ end
138
+
139
+ method = @resource.method(m.method)
140
+
141
+ # Check for nil params when method is expecting 1 or more arguments
142
+ if (method.arity != 0 && m.params.nil?)
143
+ raise ArgumentError.new("wrong number of arguments to `#{m.method}'" \
144
+ "(0 for #{method.arity})")
145
+ end
146
+
147
+ # If the arity is >= 0, then the number of params should be the same as the
148
+ # arity.
149
+ #
150
+ # For variable argument methods, the arity is -n-1 where n is the number of
151
+ # required arguments. This means if the arity is < -1, there must be at least
152
+ # (artiy.abs - 1) arguments (NOTE: if there are no required arguments, there's
153
+ # nothing to check).
154
+ if (method.arity >= 0 and method.arity != m.params.length) or
155
+ (method.arity < -1 and (method.arity.abs - 1) > m.params.length)
156
+ raise ArgumentError.new("wrong number of arguments to `#{m.method}'" \
157
+ "(#{m.params.length} for #{method.arity})")
158
+ end
159
+
160
+ method
161
+ end
162
+
163
+ EXIT_PREFIX = 'exit'
164
+
165
+ def exit_token
166
+ # Construct an exit token for this resource
167
+ "#{EXIT_PREFIX}.#{Socket.gethostname}.#{Process.pid}.#{@thread.object_id}"
168
+ end
169
+
170
+ def is_exit_token(token)
171
+ # Exit tokens are strings which can be search with a regular expresion.
172
+ return false unless token.respond_to? :match
173
+ token.match /^#{EXIT_PREFIX}/
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'method'
2
+
3
+ module LiveResource
4
+ class RemoteMethodForward
5
+ attr_reader :resource, :method, :params, :next
6
+
7
+ def initialize(resource, method, params)
8
+ @resource = resource
9
+ @method = method
10
+ @params = params
11
+ @next = nil
12
+ end
13
+
14
+ def continue(resource, method, *params)
15
+ @next = self.class.new(resource, method, params)
16
+ self
17
+ end
18
+
19
+ def inspect
20
+ "#{self.class}: #{@resource} #{@method} (#{@params.length} params)"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ module LiveResource
2
+ # Returned from async method calls, in order to later get a method's
3
+ # return value.
4
+ class Future
5
+ def initialize(proxy, method)
6
+ @proxy = proxy
7
+ @method = method
8
+ @value = nil
9
+ end
10
+
11
+ def value(timeout = 0)
12
+ if @value.nil?
13
+ @value = @proxy.wait_for_done(@method, timeout)
14
+ end
15
+
16
+ @value
17
+ end
18
+
19
+ def done?
20
+ if @value.nil?
21
+ @proxy.done_with? @method
22
+ else
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,93 @@
1
+ require 'yaml'
2
+ require_relative 'token'
3
+ require_relative 'forward'
4
+
5
+ module LiveResource
6
+ class RemoteMethod
7
+ attr_reader :flags, :path
8
+ attr_accessor :token
9
+
10
+ def initialize(params)
11
+ @path = params[:path]
12
+ @token = params[:token]
13
+ @flags = params[:flags] || {}
14
+
15
+ if @path.nil?
16
+ unless params[:method]
17
+ raise ArgumentError.new("RemoteMethod must have a method")
18
+ end
19
+
20
+ @path = []
21
+ @path << {
22
+ :method => params[:method],
23
+ :params => (params[:params] || []) }
24
+ end
25
+ end
26
+
27
+ def method
28
+ @path[0][:method]
29
+ end
30
+
31
+ def params
32
+ @path[0][:params]
33
+ end
34
+
35
+ def params=(new_params)
36
+ @path[0][:params] = new_params
37
+ end
38
+
39
+ def add_destination(proxy, method, params)
40
+ @path << {
41
+ :resource => proxy,
42
+ :method => method,
43
+ :params => params }
44
+
45
+ self
46
+ end
47
+
48
+ def forward_to(forward)
49
+ while forward
50
+ @path << {
51
+ :resource => forward.resource,
52
+ :method => forward.method,
53
+ :params => forward.params }
54
+
55
+ forward = forward.next
56
+ end
57
+
58
+ self
59
+ end
60
+
61
+ def next_destination!
62
+ @path.shift
63
+ @path[0][:resource]
64
+ end
65
+
66
+ def final_destination?
67
+ @path.length == 1
68
+ end
69
+
70
+ def inspect
71
+ if @path.length == 1
72
+ "#{self.class}: #{@path[0][:method]} (#{@path[0][:params].length} params)"
73
+ else
74
+ "#{self.class}: #{@path.length} path elements"
75
+ end
76
+ end
77
+
78
+ def encode_with coder
79
+ coder.tag = '!live_resource:method'
80
+ coder['flags'] = @flags
81
+ coder['path'] = @path
82
+ coder['token'] = @token if @token
83
+ end
84
+ end
85
+ end
86
+
87
+ # Make YAML parser create Method objects from our custom type.
88
+ Psych.add_domain_type('live_resource', 'method') do |type, data|
89
+ # Convert string keys to symbols
90
+ data = Hash[data.map { |k,v| [k.to_sym, v] }]
91
+
92
+ LiveResource::RemoteMethod.new(data)
93
+ end
@@ -0,0 +1,22 @@
1
+ require 'yaml'
2
+
3
+ module LiveResource
4
+ class RemoteMethodToken
5
+ attr_reader :redis_class, :redis_name, :seq
6
+
7
+ def initialize(redis_class, redis_name, seq)
8
+ @redis_class = redis_class
9
+ @redis_name = redis_name
10
+ @seq = seq
11
+ end
12
+
13
+ def encode_with coder
14
+ coder.represent_scalar '!live_resource:token', "#{@redis_class}.#{@redis_name}.#{@seq}"
15
+ end
16
+ end
17
+ end
18
+
19
+ # Make YAML parser create Method objects from our custom type.
20
+ Psych.add_domain_type('live_resource', 'token') do |type, data|
21
+ LiveResource::RemoteMethodToken.new(*(data.split '.'))
22
+ end