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