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