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