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
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), 'benchmark_helper')
2
+
3
+ class Resource
4
+ include LiveResource::Attribute
5
+
6
+ remote_accessor :attribute
7
+
8
+ def initialize
9
+ self.namespace = 'test'
10
+ end
11
+ end
12
+
13
+ class AttributeTest < Test::Unit::TestCase
14
+ def setup
15
+ Redis.new.flushall
16
+ end
17
+
18
+ def run_with_threads(n_threads, n_total, &block)
19
+ threads = []
20
+
21
+ n_threads.times do
22
+ threads << Thread.new do
23
+ (n_total / n_threads).times { block.call }
24
+ end
25
+ end
26
+
27
+ threads.each { |t| t.join }
28
+ end
29
+
30
+ def test_attribute_performance
31
+ resource = Resource.new
32
+ n = 10000
33
+
34
+ puts "Attribute get/set performance".title
35
+
36
+ Benchmark.bm do |x|
37
+ x.report("attr read (n=#{n})".pad) do
38
+ n.times { resource.attribute }
39
+ end
40
+
41
+ x.report("attr write (n=#{n})".pad) do
42
+ n.times { resource.attribute = 1 }
43
+ end
44
+
45
+ [1, 5, 10].each do |threads|
46
+ x.report("attr read (n=#{n}, #{threads} threads):".pad) do
47
+ run_with_threads(threads, n) { resource.attribute }
48
+ end
49
+
50
+ x.report("attr write (n=#{n}, #{threads} threads):".pad) do
51
+ run_with_threads(threads, n) { resource.attribute = 1 }
52
+ end
53
+ end
54
+ end
55
+
56
+ assert true
57
+ end
58
+ end
@@ -0,0 +1,89 @@
1
+ require File.join(File.dirname(__FILE__), 'benchmark_helper')
2
+
3
+ class Supervisor
4
+ def main(total_jobs, max_workers)
5
+ redis = Redis.new
6
+ mutex = Mutex.new
7
+ workers = 0
8
+
9
+ # Very roughly simulate having a pool of worker threads.
10
+ loop do
11
+ return if (redis.llen("results") == total_jobs)
12
+
13
+ Thread.pass while (mutex.synchronize { workers >= max_workers })
14
+
15
+ Thread.new do
16
+ mutex.synchronize { workers += 1 }
17
+ # puts "Doing work (#{workers} workers)"
18
+ Worker.new.work(1)
19
+ mutex.synchronize { workers -= 1 }
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ class Worker
26
+ def work(loops)
27
+ redis = Redis.new
28
+
29
+ loops.times do
30
+ job = redis.brpop "work", 0
31
+ redis.lpush "results", job
32
+ end
33
+ end
34
+ end
35
+
36
+ class ThreadTest < Test::Unit::TestCase
37
+ def setup
38
+ Redis.new.flushall
39
+ end
40
+
41
+ def run_single_thread(jobs)
42
+ redis = Redis.new
43
+
44
+ jobs.times do
45
+ redis.lpush "work", "foo"
46
+ end
47
+
48
+ thread = Thread.new do
49
+ Worker.new.work(jobs)
50
+ end
51
+
52
+ thread.join
53
+ end
54
+
55
+ def run_thread_spawn_on_demand(jobs, threads)
56
+ redis = Redis.new
57
+
58
+ jobs.times do
59
+ redis.lpush "work", "foo"
60
+ end
61
+
62
+ Supervisor.new.main(jobs, threads)
63
+ end
64
+
65
+ def test_thread_performance
66
+ n = 10000
67
+ threads = 10
68
+
69
+ puts "Redis push/pop performance, single thread vs. multi".title
70
+
71
+ # Test which is faster: run one thread sitting on Redis vs. many
72
+ # threads (one per job) dogpiling on Redis. That is, is the cost
73
+ # of spawning one thread per job more expensive than the IO to
74
+ # Redis? (On my machine, thread pool is nearly 4x faster. -jdc)
75
+ Benchmark.bm do |x|
76
+ x.report("single thread (n=#{n}):".pad) do
77
+ run_single_thread(n)
78
+ end
79
+
80
+ [1, 2, 5, 10].each do |threads|
81
+ x.report("thread pool (n=#{n}, #{threads} threads):".pad) do
82
+ run_thread_spawn_on_demand(n, threads)
83
+ end
84
+ end
85
+ end
86
+
87
+ assert true
88
+ end
89
+ end
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'lib/live_resource'
3
+
4
+ class FavoriteColorPublisher
5
+ include LiveResource::Attribute
6
+
7
+ remote_writer :favorite
8
+ end
9
+
10
+ publisher = FavoriteColorPublisher.new
11
+ publisher.namespace = "color"
12
+ publisher.favorite = "blue"
13
+
14
+ class FavoriteColor
15
+ include LiveResource::Attribute
16
+
17
+ remote_reader :favorite
18
+ end
19
+
20
+ reader = FavoriteColor.new
21
+ reader.namespace = "color"
22
+ puts reader.favorite # --> "blue"
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'lib/live_resource'
3
+
4
+ class FavoriteColorPublisher
5
+ include LiveResource::Attribute
6
+
7
+ remote_accessor :favorite
8
+
9
+ # Update favorite color to anything except the currently-published
10
+ # favorite.
11
+ def update_favorite
12
+ colors = ['red', 'blue', 'green']
13
+
14
+ remote_modify(:favorite) do |current_favorite|
15
+ colors.delete(current_favorite)
16
+
17
+ # Value of block will become the new value of the attribute.
18
+ colors.shuffle.first
19
+ end
20
+ end
21
+ end
22
+
23
+ color = FavoriteColorPublisher.new
24
+ color.namespace = "color"
25
+ color.favorite = "blue"
26
+
27
+ 10.times do
28
+ puts "Current fave: #{color.favorite}"
29
+ color.update_favorite
30
+ end
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ require 'lib/live_resource'
3
+
4
+ class FavoriteColorPublisher
5
+ include LiveResource::Attribute
6
+
7
+ remote_writer :favorite
8
+ end
9
+
10
+ publisher = FavoriteColorPublisher.new
11
+ publisher.namespace = "color"
12
+ publisher.favorite = "blue"
13
+
14
+ class FavoriteColorSubscriber
15
+ include LiveResource::Subscriber
16
+
17
+ remote_subscription :favorite
18
+
19
+ def favorite(new_favorite)
20
+ puts "Publisher changed its favorite to #{new_favorite}"
21
+ end
22
+ end
23
+
24
+ subscriber = FavoriteColorSubscriber.new
25
+ subscriber.namespace = "color"
26
+ subscriber.subscribe # Spawns thread
27
+
28
+ # Publisher object from the "Attribute" section above.
29
+ publisher.favorite = "red"
30
+ publisher.favorite = "green"
31
+
32
+ subscriber.unsubscribe
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'lib/live_resource'
3
+
4
+ class Server
5
+ include LiveResource::MethodProvider
6
+
7
+ remote_method :divide
8
+
9
+ def divide(dividend, divisor)
10
+ raise ArgumentError.new("cannot divide by zero") if divisor == 0
11
+
12
+ dividend / divisor
13
+ end
14
+ end
15
+
16
+ s = Server.new
17
+ s.namespace = "math"
18
+ s.logger.level = Logger::INFO
19
+
20
+ Signal.trap("INT") { s.stop_method_dispatcher }
21
+
22
+ s.start_method_dispatcher.join
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'lib/live_resource'
3
+
4
+ class Server
5
+ include LiveResource::MethodProvider
6
+
7
+ remote_method :divide
8
+
9
+ def divide(dividend, divisor)
10
+ raise ArgumentError.new("cannot divide by zero") if divisor == 0
11
+
12
+ dividend / divisor
13
+ end
14
+ end
15
+
16
+ class Client
17
+ include LiveResource::MethodSender
18
+
19
+ def fancy_process(a, b)
20
+ begin
21
+ puts remote_send :divide, a, b
22
+ rescue ArgumentError => e
23
+ puts "oops, I messed up: #{e}"
24
+ end
25
+ end
26
+ end
27
+
28
+ s = Server.new
29
+ s.namespace = "math"
30
+ s.start_method_dispatcher
31
+
32
+ c = Client.new
33
+ c.namespace = "math"
34
+ c.fancy_process(10, 5)
35
+ c.fancy_process(1, 0)
36
+
37
+ s.stop_method_dispatcher
@@ -0,0 +1,98 @@
1
+ require File.join(File.dirname(__FILE__), 'common')
2
+
3
+ module LiveResource
4
+ module Subscriber
5
+ include LiveResource::Common
6
+
7
+ UNSUBSCRIBE_KEY = :unsubscribe_key
8
+
9
+ def subscribe
10
+ ready = false
11
+ subscriptions = self.class.instance_variable_get :@subscriptions
12
+ channels = [UNSUBSCRIBE_KEY] + subscriptions.keys
13
+
14
+ @thread = Thread.new do
15
+ redis_space.subscribe(channels) do |on|
16
+ on.subscribe do |key, total|
17
+ debug "Subscribed to #{key} (#{total} subscriptions)"
18
+
19
+ if (channels.length == total)
20
+ debug "Subscriber ready"
21
+ ready = true
22
+ end
23
+ end
24
+
25
+ on.message do |key, new_value|
26
+ # Need to strip namespace from key; callbacks were registered
27
+ # before namespace was known (at initialize).
28
+ namespace_length = @namespace.length + 1 # Include '.' separator
29
+ key = key.to_s
30
+ key = key[namespace_length, key.length - namespace_length]
31
+ key = key.to_sym
32
+
33
+ # De-serialize value
34
+ new_value = new_value.nil? ? nil : YAML::load(new_value)
35
+
36
+ debug "#{key.inspect} changed value to #{new_value.inspect}"
37
+
38
+ if key.to_s.end_with? UNSUBSCRIBE_KEY.to_s
39
+ redis_space.unsubscribe
40
+ next
41
+ end
42
+
43
+ m = subscriptions[key]
44
+
45
+ if m.nil?
46
+ warn "Received subscription update for unknown key #{key}"
47
+ next
48
+ end
49
+
50
+ # Need to send method on new thread, as that method may do
51
+ # additional LiveResource/Redis operations, and this thread's
52
+ # Redis client is in subscribe mode.
53
+ Thread.new { send m, new_value }
54
+ end
55
+
56
+ on.unsubscribe do |key, total|
57
+ debug "Unsubscribed from #{key} (#{total} subscriptions)"
58
+ end
59
+
60
+ debug "Subscriber thread started"
61
+ end
62
+
63
+ debug "Subscriber thread done"
64
+ end
65
+
66
+ Thread.pass while !ready
67
+
68
+ @thread
69
+ end
70
+
71
+ def unsubscribe
72
+ # Need to publish on secondary RedisSpace because the first one is
73
+ # blocked waiting for subscription updates.
74
+ @rs_secondary = redis_space.clone
75
+ @rs_secondary.publish UNSUBSCRIBE_KEY, true
76
+ @thread.join
77
+ end
78
+
79
+ def self.included(base)
80
+ base.extend(ClassMethods)
81
+ end
82
+
83
+ module ClassMethods
84
+ def remote_subscription(attribute, method = nil)
85
+ @subscriptions ||= Hash.new
86
+
87
+ if @subscriptions[attribute]
88
+ throw ArgumentError, "Subscription callback already defined for attribute #{attribute}"
89
+ end
90
+
91
+ # If method isn't specified, assume it matches the attribute name.
92
+ method ||= attribute
93
+
94
+ @subscriptions[attribute.to_sym] = method.to_sym
95
+ end
96
+ end
97
+ end
98
+ end
data/old/redis_test.rb ADDED
@@ -0,0 +1,127 @@
1
+ require_relative 'test_helper'
2
+
3
+ class FancyClass
4
+ attr_reader :value
5
+
6
+ def initialize(value)
7
+ @value = value
8
+ end
9
+ end
10
+
11
+ class RedisClientTest < Test::Unit::TestCase
12
+ def setup
13
+ Redis.new.flushall
14
+ end
15
+
16
+ def test_global_redis_space
17
+ assert_equal LiveResource::RedisClient, LiveResource::redis.class
18
+ end
19
+
20
+ # def test_method_get_set_with_same_key
21
+ # rc = mock()
22
+ # rc.expects :attribute_set, "--- 123\n...\n"
23
+ #
24
+ #
25
+ # logger = Logger.new(STDOUT)
26
+ # logger.level = Logger::WARN
27
+ # r = LiveResource::Client.new('test', logger)
28
+ #
29
+ # # Set with same token, differing keys
30
+ # rs.method_set 123, 'key 1', 'value 1'
31
+ # rs.method_set 123, 'key 2', FancyClass.new(42)
32
+ #
33
+ # assert_equal 'value 1', rs.method_get(123, 'key 1')
34
+ # assert_equal 42, rs.method_get(123, 'key 2').value
35
+ # end
36
+ #
37
+ # def test_method_tokens_independent
38
+ # rs = LiveResource::RedisSpace.new('test')
39
+ #
40
+ # # Set several value, same keys but different tokens
41
+ # rs.method_set 1, 'key', 'value 1'
42
+ # rs.method_set 2, 'key', 'value 2'
43
+ # rs.method_set 3, 'key', 'value 3'
44
+ #
45
+ # assert_equal 'value 1', rs.method_get(1, 'key')
46
+ # assert_equal 'value 2', rs.method_get(2, 'key')
47
+ # assert_equal 'value 3', rs.method_get(3, 'key')
48
+ # end
49
+ #
50
+ # def test_method_set_exclusive
51
+ # rs = LiveResource::RedisSpace.new('test')
52
+ #
53
+ # assert_equal true, rs.method_set_exclusive(1, 'key', 'value 1')
54
+ # assert_equal false, rs.method_set_exclusive(1, 'key', 'value 2')
55
+ # end
56
+ #
57
+ # def test_method_push_pop_simple
58
+ # rs = LiveResource::RedisSpace.new('test')
59
+ # assert_equal 0, Redis.new.dbsize
60
+ #
61
+ # rs.method_push '1'
62
+ # assert_equal 1, Redis.new.dbsize
63
+ #
64
+ # token = rs.method_wait
65
+ # assert_equal '1', token
66
+ #
67
+ # rs.method_done token
68
+ # assert_equal 0, Redis.new.dbsize
69
+ # end
70
+ #
71
+ # def test_method_push_pop_multiple
72
+ # rs = LiveResource::RedisSpace.new('test')
73
+ # assert_equal 0, Redis.new.dbsize
74
+ #
75
+ # rs.method_push '1'
76
+ # rs.method_push '2'
77
+ # rs.method_push '3'
78
+ #
79
+ # assert_equal ['3', '2', '1'], rs.method_tokens_waiting
80
+ # assert_equal [], rs.method_tokens_in_progress
81
+ #
82
+ # assert_equal '1', rs.method_wait
83
+ # assert_equal '2', rs.method_wait
84
+ # assert_equal '3', rs.method_wait
85
+ #
86
+ # assert_equal [], rs.method_tokens_waiting
87
+ # assert_equal ['3', '2', '1'], rs.method_tokens_in_progress
88
+ #
89
+ # # Redis should have one key here (the methods_in_progress list)
90
+ # assert_equal 1, Redis.new.dbsize
91
+ #
92
+ # # Now start marking methods done; after the last one, the key
93
+ # # count should go down to zero.
94
+ # rs.method_done '1'
95
+ # assert_equal 1, Redis.new.dbsize
96
+ # rs.method_done '2'
97
+ # assert_equal 1, Redis.new.dbsize
98
+ # rs.method_done '3'
99
+ # assert_equal 0, Redis.new.dbsize
100
+ # end
101
+ #
102
+ # def test_serializes_exceptions_properly
103
+ # rs = LiveResource::RedisSpace.new('test')
104
+ #
105
+ # rs.method_set('1', 'key', 'value') # just need something there
106
+ # rs.result_set('1', RuntimeError.new('foo'))
107
+ # result = rs.result_get '1'
108
+ #
109
+ # assert_equal RuntimeError, result.class
110
+ # assert_equal 'foo', result.message
111
+ # end
112
+ #
113
+ # def test_find_token_in_lists
114
+ # r = Redis.new
115
+ # rs = LiveResource::RedisSpace.new('test')
116
+ #
117
+ # r.lpush('test.methods', '1')
118
+ # r.lpush('test.methods_in_progress', '2')
119
+ # r.lpush('test.results.3', 'result')
120
+ #
121
+ # assert_equal :methods, rs.find_token('1')
122
+ # assert_equal :methods_in_progress, rs.find_token('2')
123
+ # assert_equal :results, rs.find_token('3')
124
+ # assert_equal nil, rs.find_token('4')
125
+ # end
126
+ end
127
+