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