liveresource 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/BSDL +24 -0
- data/COPYING +59 -0
- data/GPL +339 -0
- data/README.md +289 -0
- data/Rakefile +47 -0
- data/benchmark/benchmark_helper.rb +19 -0
- data/benchmark/method_benchmark.rb +94 -0
- data/lib/live_resource.rb +66 -0
- data/lib/live_resource/attributes.rb +77 -0
- data/lib/live_resource/declarations.rb +200 -0
- data/lib/live_resource/finders.rb +43 -0
- data/lib/live_resource/log_helper.rb +24 -0
- data/lib/live_resource/methods.rb +41 -0
- data/lib/live_resource/methods/dispatcher.rb +176 -0
- data/lib/live_resource/methods/forward.rb +23 -0
- data/lib/live_resource/methods/future.rb +27 -0
- data/lib/live_resource/methods/method.rb +93 -0
- data/lib/live_resource/methods/token.rb +22 -0
- data/lib/live_resource/redis_client.rb +100 -0
- data/lib/live_resource/redis_client/attributes.rb +40 -0
- data/lib/live_resource/redis_client/methods.rb +194 -0
- data/lib/live_resource/redis_client/registration.rb +25 -0
- data/lib/live_resource/resource.rb +44 -0
- data/lib/live_resource/resource_proxy.rb +180 -0
- data/old/benchmark/attribute_benchmark.rb +58 -0
- data/old/benchmark/thread_benchmark.rb +89 -0
- data/old/examples/attribute.rb +22 -0
- data/old/examples/attribute_rmw.rb +30 -0
- data/old/examples/attribute_subscriber.rb +32 -0
- data/old/examples/method_provider_sleep.rb +22 -0
- data/old/examples/methods.rb +37 -0
- data/old/lib/live_resource/subscriber.rb +98 -0
- data/old/redis_test.rb +127 -0
- data/old/state_publisher_test.rb +139 -0
- data/old/test/attribute_modify_test.rb +52 -0
- data/old/test/attribute_options_test.rb +54 -0
- data/old/test/attribute_subscriber_test.rb +94 -0
- data/old/test/composite_resource_test.rb +61 -0
- data/old/test/method_sender_test.rb +41 -0
- data/old/test/redis_api_test.rb +185 -0
- data/old/test/simple_attribute_test.rb +75 -0
- data/test/attribute_test.rb +212 -0
- data/test/declarations_test.rb +119 -0
- data/test/logger_test.rb +44 -0
- data/test/method_call_test.rb +223 -0
- data/test/method_forward_continue_test.rb +83 -0
- data/test/method_params_test.rb +81 -0
- data/test/method_routing_test.rb +59 -0
- data/test/multiple_class_test.rb +47 -0
- data/test/new_api_DISABLED.rb +127 -0
- data/test/test_helper.rb +9 -0
- data/test/volume_create_DISABLED.rb +74 -0
- 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
|