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