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
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/package_task'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
desc "Default Task"
|
7
|
+
task :default => [:test]
|
8
|
+
|
9
|
+
Rake::TestTask.new :test do |test|
|
10
|
+
test.verbose = false
|
11
|
+
test.test_files = ['test/*_test.rb'].sort
|
12
|
+
end
|
13
|
+
|
14
|
+
Rake::TestTask.new :benchmark do |benchmark|
|
15
|
+
benchmark.verbose = false
|
16
|
+
# benchmark.options = '--verbose=s'
|
17
|
+
benchmark.test_files = ['benchmark/*_benchmark.rb']
|
18
|
+
end
|
19
|
+
|
20
|
+
YARD::Rake::YardocTask.new do |t|
|
21
|
+
t.files = ['lib/**/*.rb']
|
22
|
+
end
|
23
|
+
|
24
|
+
task :clean do
|
25
|
+
FileUtils.rm_rf 'pkg'
|
26
|
+
end
|
27
|
+
|
28
|
+
gem_spec = Gem::Specification.new do |spec|
|
29
|
+
spec.name = 'liveresource'
|
30
|
+
spec.summary = 'Live Resource'
|
31
|
+
spec.version = '2.0.0'
|
32
|
+
spec.author = 'Spectra Logic'
|
33
|
+
spec.email = 'public@joshcarter.com'
|
34
|
+
spec.homepage = 'https://github.com/joshcarter/liveresource'
|
35
|
+
spec.description = 'Remote-callable attributes and methods for ' \
|
36
|
+
'IPC and cluster use.'
|
37
|
+
|
38
|
+
spec.files = `git ls-files`.split("\n")
|
39
|
+
|
40
|
+
spec.add_dependency 'redis'
|
41
|
+
spec.add_development_dependency 'yard'
|
42
|
+
end
|
43
|
+
|
44
|
+
gem = Gem::PackageTask.new(gem_spec) do |pkg|
|
45
|
+
pkg.need_tar = false
|
46
|
+
pkg.need_zip = false
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'thread'
|
4
|
+
require 'pp'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
require_relative '../lib/live_resource'
|
8
|
+
|
9
|
+
Thread.abort_on_exception = true
|
10
|
+
|
11
|
+
class String
|
12
|
+
def pad(pad_to = 70)
|
13
|
+
self.ljust(pad_to)
|
14
|
+
end
|
15
|
+
|
16
|
+
def title
|
17
|
+
"*\n* #{self}\n*\n"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative 'benchmark_helper'
|
2
|
+
|
3
|
+
class Server
|
4
|
+
include LiveResource::Resource
|
5
|
+
|
6
|
+
resource_class :server
|
7
|
+
resource_name :object_id
|
8
|
+
|
9
|
+
def test_method
|
10
|
+
42
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class MethodTest < Test::Unit::TestCase
|
15
|
+
def setup
|
16
|
+
Redis.new.flushall
|
17
|
+
LiveResource::register Server.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
LiveResource::stop
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_sync_method_performance
|
25
|
+
n = 1000
|
26
|
+
|
27
|
+
puts "Synchronous method call performance".title
|
28
|
+
|
29
|
+
[1, 5, 10].each do |threads|
|
30
|
+
b = Benchmark.measure do
|
31
|
+
run_sync(n, threads)
|
32
|
+
end
|
33
|
+
|
34
|
+
puts ("n=#{n}, #{threads} threads: #{b.to_s.strip} " +
|
35
|
+
sprintf("%.0f m/sec", n / b.total)).pad
|
36
|
+
end
|
37
|
+
|
38
|
+
assert true
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_async_method_performance
|
42
|
+
n = 1000
|
43
|
+
|
44
|
+
puts "Asynchronous method call performance".title
|
45
|
+
|
46
|
+
[1, 10, 100].each do |batch_size|
|
47
|
+
b = Benchmark.measure do
|
48
|
+
run_async(n, batch_size)
|
49
|
+
end
|
50
|
+
|
51
|
+
puts ("n=#{n}, batches of #{batch_size}: #{b.to_s.strip} " +
|
52
|
+
sprintf("%.0f m/sec", n / b.total)).pad
|
53
|
+
end
|
54
|
+
|
55
|
+
assert true
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_sync(n, n_threads)
|
59
|
+
threads = []
|
60
|
+
|
61
|
+
n_threads.times do
|
62
|
+
threads << Thread.new do
|
63
|
+
server = LiveResource::any(:server)
|
64
|
+
|
65
|
+
(n / n_threads).times { server.test_method }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
threads.each { |t| t.join }
|
70
|
+
end
|
71
|
+
|
72
|
+
def run_async(n, batch_size)
|
73
|
+
server = LiveResource::any(:server)
|
74
|
+
futures = Queue.new
|
75
|
+
|
76
|
+
# Calls
|
77
|
+
send_thread = Thread.new do
|
78
|
+
n.times do
|
79
|
+
Thread.pass while (futures.length >= batch_size)
|
80
|
+
|
81
|
+
futures << server.test_method?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Results
|
86
|
+
n.times do
|
87
|
+
Thread.pass while futures.empty?
|
88
|
+
|
89
|
+
futures.pop.value
|
90
|
+
end
|
91
|
+
|
92
|
+
send_thread.join
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative 'live_resource/resource'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
# LiveResource is a framework for coordinating processes and status
|
5
|
+
# within a distributed system. Consult the documention for
|
6
|
+
# LiveResource::Resource for attribute and method providers,
|
7
|
+
# LiveResource::Finders for discovering resources, and
|
8
|
+
# LiveResource::ResourceProxy for using resources.
|
9
|
+
module LiveResource
|
10
|
+
# Register the resource, allowing its discovery and methods to be
|
11
|
+
# called on it. This method will block until the resource is fully
|
12
|
+
# registered and its method dispatcher is running.
|
13
|
+
#
|
14
|
+
# @param resource [LiveResource::Resource] the object to register
|
15
|
+
def self.register(resource)
|
16
|
+
# puts "registering #{resource.to_s}"
|
17
|
+
|
18
|
+
@@resources ||= Set.new
|
19
|
+
@@resources << resource
|
20
|
+
|
21
|
+
resource.start
|
22
|
+
end
|
23
|
+
|
24
|
+
# Unregister the resource, removing it from discovery and stopping
|
25
|
+
# its method dispatcher. This method will block until the method
|
26
|
+
# dispatcher is stopped.
|
27
|
+
#
|
28
|
+
# @param resource [LiveResource::Resource] the object to unregister
|
29
|
+
def self.unregister(resource)
|
30
|
+
# puts "unregistering #{resource.to_s}"
|
31
|
+
|
32
|
+
resource.stop
|
33
|
+
|
34
|
+
@@resources.delete resource
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start all resources. Usually not needed since registering a
|
38
|
+
# resource automatically starts it; however if you stopped
|
39
|
+
# LiveResource manually, this will let you re-start all registered
|
40
|
+
# resources.
|
41
|
+
def self.start
|
42
|
+
@@resources.each do |resource|
|
43
|
+
resource.start
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Stop all resources, preventing methods from being called on them.
|
48
|
+
def self.stop
|
49
|
+
@@resources.each do |resource|
|
50
|
+
resource.stop
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Run LiveResource until the exit_signal (default=SIGINT) is recevied.
|
55
|
+
# Optionally invoke the exit callback before exiting.
|
56
|
+
def self.run(exit_signal="INT", &exit_cb)
|
57
|
+
Signal.trap(exit_signal) do
|
58
|
+
self.stop
|
59
|
+
yield if exit_cb
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
|
63
|
+
# Put this thread to sleep
|
64
|
+
sleep
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module LiveResource
|
2
|
+
module Attributes
|
3
|
+
def redis
|
4
|
+
@_redis ||= RedisClient.new(resource_class, resource_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def remote_attributes
|
8
|
+
if self.is_a? Class
|
9
|
+
[]
|
10
|
+
else
|
11
|
+
self.class.remote_instance_attributes
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def remote_attribute_read(key, options = {})
|
16
|
+
redis.attribute_read(key, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def remote_attribute_write(key, value, options = {})
|
20
|
+
if (key.to_sym == self.class.resource_name_attr) and !self.is_a?(Class)
|
21
|
+
@_redis = RedisClient.new(resource_class, value)
|
22
|
+
end
|
23
|
+
|
24
|
+
redis.attribute_write(key, value, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Write a new value to an attribute if it doesn't exist yet.
|
28
|
+
def remote_attribute_writenx(key, value)
|
29
|
+
remote_attribute_write(key, value, no_overwrite: true)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Modify an attribute or set of attributes based on the current value(s).
|
33
|
+
# Uses the optimistic locking mechanism provided by Redis WATCH/MULTI/EXEC
|
34
|
+
# transactions.
|
35
|
+
#
|
36
|
+
# The user passes in the a block which will be used to update the attribute(s).
|
37
|
+
# Since the block may need to be replayed, the user should not update any
|
38
|
+
# external state that relies on the block executing only once.
|
39
|
+
def remote_attribute_modify(*attributes, &block)
|
40
|
+
invalid_attrs = attributes - redis.registered_attributes
|
41
|
+
unless invalid_attrs.empty?
|
42
|
+
raise ArgumentError.new("remote_modify: no such attribute(s) '#{invalid_attrs}'")
|
43
|
+
end
|
44
|
+
|
45
|
+
unless block
|
46
|
+
raise ArgumentError.new("remote_modify requires a block")
|
47
|
+
end
|
48
|
+
|
49
|
+
# Optimistic locking implemented along the lines of:
|
50
|
+
# http://redis.io/topics/transactions
|
51
|
+
loop do
|
52
|
+
# Gather up the attributes and their new values
|
53
|
+
mods = attributes.map do |a|
|
54
|
+
# Watch/get the value
|
55
|
+
redis.attribute_watch(a)
|
56
|
+
v = redis.attribute_read(a)
|
57
|
+
|
58
|
+
# Block modifies the value
|
59
|
+
v = block.call(a, v)
|
60
|
+
[a, v]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Start the transaction
|
64
|
+
redis.multi
|
65
|
+
|
66
|
+
mods.each do |mod|
|
67
|
+
# Set to new value; if ok, we're done.
|
68
|
+
redis.attribute_write(mod[0], mod[1])
|
69
|
+
end
|
70
|
+
|
71
|
+
# Attempt to execute the transaction. Otherwise we'll loop and
|
72
|
+
# try again with the new value.
|
73
|
+
break if redis.exec
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module LiveResource
|
4
|
+
module Declarations
|
5
|
+
def resource_name
|
6
|
+
# Getting resource name may be expensive, e.g. if it's coming
|
7
|
+
# from Redis. Cache so we don't re-fectch this resource's name
|
8
|
+
# more than once.
|
9
|
+
return @_cached_resource_name if @_cached_resource_name
|
10
|
+
|
11
|
+
# Class-level resource_name is an attribute we fetch to determine
|
12
|
+
# the instance's name
|
13
|
+
attr = self.class.resource_name_attr
|
14
|
+
|
15
|
+
if attr
|
16
|
+
@_cached_resource_name = self.send(attr)
|
17
|
+
else
|
18
|
+
raise "can't get resource name for #{self.class.to_s}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def resource_class
|
23
|
+
self.class.instance_variable_get(:@_resource_class)
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def self.extended(base)
|
28
|
+
class << base
|
29
|
+
# Override the regular new routine with a custom new
|
30
|
+
# which auto-registers the resource.
|
31
|
+
alias :ruby_new :new
|
32
|
+
|
33
|
+
def new(*params)
|
34
|
+
obj = ruby_new(*params)
|
35
|
+
LiveResource::register obj
|
36
|
+
obj
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# FIXME: comment this
|
42
|
+
def resource_name(attribute_name = nil)
|
43
|
+
if attribute_name
|
44
|
+
# Called from class definition to set the attribute from which we get a resource's name.
|
45
|
+
@_resource_name = attribute_name.to_sym
|
46
|
+
else
|
47
|
+
# Get the class-level resource name.
|
48
|
+
@_resource_class
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# FIXME: comment this
|
53
|
+
def resource_class(class_name = nil)
|
54
|
+
if class_name
|
55
|
+
# Called from class definition to set the resource's class.
|
56
|
+
@_resource_class = class_name.to_sym
|
57
|
+
else
|
58
|
+
# Get the class-level resource class, which we'll always call :class.
|
59
|
+
:class
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the attribute which defines this resource's name. (For internal use only.)
|
64
|
+
def resource_name_attr
|
65
|
+
@_resource_name
|
66
|
+
end
|
67
|
+
|
68
|
+
# call-seq:
|
69
|
+
# remote_reader :attr
|
70
|
+
# remote_reader :attr, { :opt => val }
|
71
|
+
# remote_reader :attr1, :attr2, :attr3
|
72
|
+
#
|
73
|
+
# Declare a remote attribute reader. A list of symbols is used
|
74
|
+
# to create multiple attribute readers.
|
75
|
+
def remote_reader(*params)
|
76
|
+
@_instance_attributes ||= Set.new
|
77
|
+
options = {}
|
78
|
+
|
79
|
+
# One symbol and one hash is treated as a reader with options;
|
80
|
+
# right now there are no reader options, so just pop them off.
|
81
|
+
if (params.length == 2) && (params.last.is_a? Hash)
|
82
|
+
options = params.pop
|
83
|
+
end
|
84
|
+
|
85
|
+
# Everything left in params should be a symbol (i.e., method name).
|
86
|
+
if params.find { |m| !m.is_a? Symbol }
|
87
|
+
raise ArgumentError.new("Invalid or ambiguous arguments to remote_reader: #{params.inspect}")
|
88
|
+
end
|
89
|
+
|
90
|
+
params.each do |m|
|
91
|
+
@_instance_attributes << m
|
92
|
+
|
93
|
+
define_method("#{m}") do
|
94
|
+
remote_attribute_read(m, options)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# call-seq:
|
100
|
+
# remote_writer :attr
|
101
|
+
# remote_writer :attr, { :opt => val }
|
102
|
+
# remote_writer :attr1, :attr2, :attr3
|
103
|
+
#
|
104
|
+
# Declare a remote attribute writer. One or more symbols are
|
105
|
+
# used to declare writers with default options. This creates
|
106
|
+
# methods matching the symbols provided, e.g.:
|
107
|
+
#
|
108
|
+
# remote_writer :attr -> def attr=(value) [...]
|
109
|
+
#
|
110
|
+
# One symbol and a hash is used to declare an attribute writer
|
111
|
+
# with options. Currently supported options:
|
112
|
+
#
|
113
|
+
# * :ttl (integer): time-to-live of attribute. After (TTL)
|
114
|
+
# seconds, the value of the attribute returns to nil.
|
115
|
+
def remote_writer(*params)
|
116
|
+
@_instance_attributes ||= Set.new
|
117
|
+
options = {}
|
118
|
+
|
119
|
+
# One symbol and one hash is treated as a writer with options.
|
120
|
+
if (params.length == 2) && (params.last.is_a? Hash)
|
121
|
+
options = params.pop
|
122
|
+
end
|
123
|
+
|
124
|
+
# Everything left in params should be a symbol (i.e., method name).
|
125
|
+
if params.find { |m| !m.is_a? Symbol }
|
126
|
+
raise ArgumentError.new("Invalid or ambiguous arguments to remote_writer: #{params.inspect}")
|
127
|
+
end
|
128
|
+
|
129
|
+
params.each do |m|
|
130
|
+
@_instance_attributes << "#{m}=".to_sym
|
131
|
+
|
132
|
+
define_method("#{m}=") do |value|
|
133
|
+
remote_attribute_write(m, value, options)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# call-seq:
|
139
|
+
# remote_accessor :attr
|
140
|
+
# remote_accessor :attr, { :opt => val }
|
141
|
+
# remote_accessor :attr1, :attr2, :attr3
|
142
|
+
#
|
143
|
+
# Declare remote attribute reader and writer. One or more symbols
|
144
|
+
# are used to declare multiple attributes, as in +remote_writer+.
|
145
|
+
# One symbol with a hash is used to declare an accessor with
|
146
|
+
# options; currently these options are only supported on the
|
147
|
+
# attribute write, and they are ignored on the attribute read.
|
148
|
+
def remote_accessor(*params)
|
149
|
+
remote_reader(*params)
|
150
|
+
remote_writer(*params)
|
151
|
+
end
|
152
|
+
|
153
|
+
def remote_instance_attributes
|
154
|
+
@_instance_attributes ||= Set.new
|
155
|
+
@_instance_attributes.to_a
|
156
|
+
end
|
157
|
+
|
158
|
+
# Remote-callable methods for an instance.
|
159
|
+
def remote_instance_methods
|
160
|
+
@_instance_methods ||= Set.new
|
161
|
+
@_instance_attributes ||= Set.new
|
162
|
+
|
163
|
+
# Remove all instance attributes, then fiter out private and
|
164
|
+
# protected methods.
|
165
|
+
(@_instance_methods - @_instance_attributes).find_all do |m|
|
166
|
+
if private_method_defined?(m) or protected_method_defined?(m)
|
167
|
+
nil
|
168
|
+
else
|
169
|
+
m
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Remote-callable methods for a resource class.
|
175
|
+
def remote_singleton_methods
|
176
|
+
@_singleton_methods ||= Set.new
|
177
|
+
c = singleton_class
|
178
|
+
|
179
|
+
# Filter out private and protected methods of the singleton class.
|
180
|
+
@_singleton_methods.find_all do |m|
|
181
|
+
if c.private_method_defined?(m) or c.protected_method_defined?(m)
|
182
|
+
nil
|
183
|
+
else
|
184
|
+
m
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def method_added(m)
|
190
|
+
@_instance_methods ||= Set.new
|
191
|
+
@_instance_methods << m
|
192
|
+
end
|
193
|
+
|
194
|
+
def singleton_method_added(m)
|
195
|
+
@_singleton_methods ||= Set.new
|
196
|
+
@_singleton_methods << m
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|