rjr 0.12.2 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/README.md +49 -36
  2. data/Rakefile +2 -0
  3. data/bin/rjr-client +11 -9
  4. data/bin/rjr-server +12 -10
  5. data/examples/amqp.rb +29 -0
  6. data/examples/client.rb +32 -0
  7. data/examples/complete.rb +36 -0
  8. data/examples/local.rb +29 -0
  9. data/examples/server.rb +26 -0
  10. data/examples/tcp.rb +29 -0
  11. data/examples/web.rb +22 -0
  12. data/examples/ws.rb +29 -0
  13. data/lib/rjr/common.rb +7 -12
  14. data/lib/rjr/dispatcher.rb +171 -239
  15. data/lib/rjr/em_adapter.rb +33 -66
  16. data/lib/rjr/message.rb +43 -12
  17. data/lib/rjr/node.rb +197 -103
  18. data/lib/rjr/nodes/amqp.rb +216 -0
  19. data/lib/rjr/nodes/easy.rb +159 -0
  20. data/lib/rjr/nodes/local.rb +118 -0
  21. data/lib/rjr/{missing_node.rb → nodes/missing.rb} +4 -2
  22. data/lib/rjr/nodes/multi.rb +79 -0
  23. data/lib/rjr/nodes/tcp.rb +211 -0
  24. data/lib/rjr/nodes/web.rb +197 -0
  25. data/lib/rjr/nodes/ws.rb +187 -0
  26. data/lib/rjr/stats.rb +70 -0
  27. data/lib/rjr/thread_pool.rb +178 -123
  28. data/site/index.html +45 -0
  29. data/site/jquery-latest.js +9404 -0
  30. data/site/jrw.js +297 -0
  31. data/site/json.js +199 -0
  32. data/specs/dispatcher_spec.rb +244 -198
  33. data/specs/em_adapter_spec.rb +52 -80
  34. data/specs/message_spec.rb +223 -197
  35. data/specs/node_spec.rb +67 -163
  36. data/specs/nodes/amqp_spec.rb +82 -0
  37. data/specs/nodes/easy_spec.rb +13 -0
  38. data/specs/nodes/local_spec.rb +72 -0
  39. data/specs/nodes/multi_spec.rb +65 -0
  40. data/specs/nodes/tcp_spec.rb +75 -0
  41. data/specs/nodes/web_spec.rb +77 -0
  42. data/specs/nodes/ws_spec.rb +78 -0
  43. data/specs/stats_spec.rb +59 -0
  44. data/specs/thread_pool_spec.rb +44 -35
  45. metadata +40 -30
  46. data/lib/rjr/amqp_node.rb +0 -330
  47. data/lib/rjr/inspect.rb +0 -65
  48. data/lib/rjr/local_node.rb +0 -150
  49. data/lib/rjr/multi_node.rb +0 -65
  50. data/lib/rjr/tcp_node.rb +0 -323
  51. data/lib/rjr/thread_pool2.rb +0 -272
  52. data/lib/rjr/util.rb +0 -104
  53. data/lib/rjr/web_node.rb +0 -266
  54. data/lib/rjr/ws_node.rb +0 -289
  55. data/lib/rjr.rb +0 -16
  56. data/specs/amqp_node_spec.rb +0 -31
  57. data/specs/inspect_spec.rb +0 -60
  58. data/specs/local_node_spec.rb +0 -43
  59. data/specs/multi_node_spec.rb +0 -45
  60. data/specs/tcp_node_spec.rb +0 -33
  61. data/specs/util_spec.rb +0 -46
  62. data/specs/web_node_spec.rb +0 -32
  63. data/specs/ws_node_spec.rb +0 -32
  64. /data/lib/rjr/{tcp_node2.rb → nodes/tcp2.rb} +0 -0
  65. /data/lib/rjr/{udp_node.rb → nodes/udp.rb} +0 -0
@@ -1,184 +1,239 @@
1
- # Thread Pool
1
+ # Thread Pool (second implementation)
2
2
  #
3
- # Copyright (C) 2010-2012 Mohammed Morsi <mo@morsi.org>
3
+ # Copyright (C) 2010-2013 Mohammed Morsi <mo@morsi.org>
4
4
  # Licensed under the Apache License, Version 2.0
5
5
 
6
- # Work item to be executed in a thread launched by pool
6
+ require 'singleton'
7
+
8
+ module RJR
9
+
10
+ # Work item to be executed in a thread launched by {ThreadPool}.
11
+ #
12
+ # The end user should initialize this class with a handle
13
+ # to the job to be executed and the params to pass to it, then
14
+ # hand the instance off to the thread pool to take care of the rest.
7
15
  class ThreadPoolJob
16
+ # Proc to be invoked to perform work
8
17
  attr_accessor :handler
18
+
19
+ # Parameters to pass to handler proc
9
20
  attr_accessor :params
10
21
 
22
+ # Time job started, if nil job hasn't started yet
23
+ attr_accessor :time_started
24
+
25
+ # Time job completed, if nil job hasn't completed yet
26
+ attr_accessor :time_completed
27
+
28
+ # Thread running the job
29
+ attr_accessor :thread
30
+
11
31
  # ThreadPoolJob initializer
12
32
  # @param [Array] params arguments to pass to the job when it is invoked
13
33
  # @param [Callable] block handle to callable object corresponding to job to invoke
14
34
  def initialize(*params, &block)
15
35
  @params = params
16
36
  @handler = block
37
+ @being_executed = false
38
+ @timestamp = nil
39
+ end
40
+
41
+ # Return bool indicating if job has started
42
+ def started?
43
+ !@time_started.nil?
44
+ end
45
+
46
+ # Return bool indicating if job has completed
47
+ def completed?
48
+ !@time_started.nil? && !@time_completed.nil?
49
+ end
50
+
51
+ # Return bool indicating if the job has started but not completed
52
+ # and the specified timeout has expired
53
+ def expired?(timeout)
54
+ !@time_started.nil? && @time_completed.nil? && ((Time.now - @time_started) > timeout)
17
55
  end
18
- end
19
56
 
57
+ # Set job metadata and execute job with specified params.
58
+ #
59
+ # Used internally by thread pool
60
+ def exec(lock)
61
+ lock.synchronize {
62
+ @thread = Thread.current
63
+ @time_started = Time.now
64
+ }
65
+
66
+ @handler.call *@params
67
+
68
+ # ensure we do not switch to another job
69
+ # before atomic check expiration / terminate
70
+ # expired threads happens below
71
+ lock.synchronize {
72
+ @time_completed = Time.now
73
+ @thread = nil
74
+ }
75
+ end
76
+ end
20
77
 
21
78
  # Utility to launches a specified number of threads on instantiation,
22
79
  # assigning work to them in order as it arrives.
23
80
  #
24
- # Supports optional timeout which allows the developer to kill and restart
81
+ # Supports optional timeout which allows the user to kill and restart
25
82
  # threads if a job is taking too long to run.
26
83
  class ThreadPool
84
+ include Singleton
27
85
 
28
- # @private
29
- # Helper class to encapsulate each thread pool thread
30
- class ThreadPoolJobRunner
31
- attr_accessor :time_started
86
+ class << self
87
+ # @!group Config options (must be set before first node is instantiated)
32
88
 
33
- def initialize(thread_pool)
34
- @thread_pool = thread_pool
35
- @timeout_lock = Mutex.new
36
- @thread_lock = Mutex.new
37
- end
89
+ # Number of threads to instantiate in local worker pool
90
+ attr_accessor :num_threads
38
91
 
39
- # Start thread and pull a work items off the thread pool work queue and execute them.
40
- #
41
- # This method will return immediately after the worker thread is started but the
42
- # thread launched will persist until {#stop} is invoked
43
- def run
44
- @thread_lock.synchronize {
45
- @thread = Thread.new {
46
- until @thread_pool.terminate
47
- @timeout_lock.synchronize { @time_started = nil }
48
- work = @thread_pool.next_job
49
- @timeout_lock.synchronize { @time_started = Time.now }
50
- unless work.nil?
51
- begin
52
- work.handler.call *work.params
53
- rescue Exception => e
54
- puts "Thread raised Fatal Exception #{e}"
55
- puts "\n#{e.backtrace.join("\n")}"
56
- end
57
- end
58
- end
59
- }
60
- }
61
- end
92
+ # Timeout after which worker threads are killed
93
+ attr_accessor :timeout
62
94
 
63
- # Return boolean indicating if worker thread is running or not
64
- def running?
65
- res = nil
66
- @thread_lock.synchronize{
67
- res = (!@thread.nil? && (@thread.status != false))
68
- }
69
- res
70
- end
95
+ # @!endgroup
96
+ end
71
97
 
72
- # Return boolean indicating if worker thread run time has exceeded timeout
73
- # Should not invoke after stop is called
74
- def check_timeout(timeout)
75
- @timeout_lock.synchronize {
76
- if !@time_started.nil? && Time.now - @time_started > timeout
77
- stop
78
- run
98
+ private
99
+
100
+ # Internal helper, launch worker thread
101
+ def launch_worker
102
+ @worker_threads << Thread.new {
103
+ while work = @work_queue.pop
104
+ begin
105
+ #RJR::Logger.debug "launch thread pool job #{work}"
106
+ @running_queue << work
107
+ work.exec(@thread_lock)
108
+ # TODO cleaner / more immediate way to pop item off running_queue
109
+ #RJR::Logger.debug "finished thread pool job #{work}"
110
+ rescue Exception => e
111
+ # FIXME also send to rjr logger at a critical level
112
+ puts "Thread raised Fatal Exception #{e}"
113
+ puts "\n#{e.backtrace.join("\n")}"
79
114
  end
115
+ end
116
+ }
117
+ end
118
+
119
+ # Internal helper, performs checks on workers
120
+ def check_workers
121
+ if @terminate
122
+ @worker_threads.each { |t|
123
+ t.kill
80
124
  }
125
+ @worker_threads = []
126
+
127
+ elsif @timeout
128
+ readd = []
129
+ while @running_queue.size > 0 && work = @running_queue.pop
130
+ # check expiration / killing expired threads must be atomic
131
+ # and mutually exclusive with the process of marking a job completed above
132
+ @thread_lock.synchronize{
133
+ if work.expired?(@timeout)
134
+ work.thread.kill
135
+ @worker_threads.delete(work.thread)
136
+ launch_worker
137
+
138
+ elsif !work.completed?
139
+ readd << work
140
+ end
141
+ }
142
+ end
143
+ readd.each { |work| @running_queue << work }
81
144
  end
145
+ end
82
146
 
83
- # Stop the worker thread being executed
84
- def stop
85
- @thread_lock.synchronize {
86
- if @thread.alive?
87
- @thread.kill
88
- @thread.join
147
+ # Internal helper, launch management thread
148
+ def launch_manager
149
+ @manager_thread = Thread.new {
150
+ until @terminate
151
+ if @timeout
152
+ sleep @timeout
153
+ check_workers
154
+ else
155
+ Thread.yield
89
156
  end
90
- @thread = nil
91
- }
92
- end
157
+ end
93
158
 
94
- # Block until the worker thread is finished
95
- def join
96
- @thread_lock.synchronize {
97
- @thread.join unless @thread.nil?
98
- }
99
- end
159
+ check_workers
160
+ }
100
161
  end
101
162
 
102
- # Create a thread pool with a specified number of threads
103
- # @param [Integer] num_threads the number of worker threads to create
104
- # @param [Hash] args optional arguments to initialize thread pool with
105
- # @option args [Integer] :timeout optional timeout to use to kill long running worker jobs
106
- def initialize(num_threads, args = {})
107
- @num_threads = num_threads
108
- @timeout = args[:timeout]
109
- @job_runners = []
110
- @job_runners_lock = Mutex.new
111
- @terminate = false
112
- @terminate_lock = Mutex.new
163
+ # Create a new thread pool
164
+ def initialize
165
+ RJR::ThreadPool.num_threads ||= 20
166
+ RJR::ThreadPool.timeout ||= 10
167
+ @num_threads = RJR::ThreadPool.num_threads
168
+ @timeout = RJR::ThreadPool.timeout
169
+ @worker_threads = []
113
170
 
114
- @work_queue = Queue.new
171
+ @work_queue = Queue.new
172
+ @running_queue = Queue.new
115
173
 
116
- 0.upto(@num_threads) { |i|
117
- runner = ThreadPoolJobRunner.new(self)
118
- @job_runners << runner
119
- runner.run
120
- }
174
+ @thread_lock = Mutex.new
175
+ @terminate = true
121
176
 
122
- # optional timeout thread
123
- unless @timeout.nil?
124
- @timeout_thread = Thread.new {
125
- until terminate
126
- sleep @timeout
127
- @job_runners_lock.synchronize {
128
- @job_runners.each { |jr|
129
- jr.check_timeout(@timeout)
130
- }
131
- }
132
- end
133
- }
134
- end
177
+ ObjectSpace.define_finalizer(self, self.class.finalize(self))
135
178
  end
136
179
 
137
- # Return boolean indicated if thread pool is running.
138
- #
139
- # If at least one worker thread isn't terminated, the pool is still considered running
140
- def running?
141
- !terminate && (@timeout.nil? || (!@timeout_thread.nil? && @timeout_thread.status)) &&
142
- @job_runners.all? { |r| r.running? }
180
+ # Ruby ObjectSpace finalizer to ensure that thread pool terminates all
181
+ # threads when object is destroyed.
182
+ def self.finalize(thread_pool)
183
+ # TODO this isn't doing much as by the time this is invoked threads will
184
+ # already be shutdown
185
+ proc { thread_pool.stop ; thread_pool.join }
143
186
  end
144
187
 
145
- # Return boolean indicating if the thread pool should be terminated
146
- def terminate
147
- @terminate_lock.synchronize { @terminate }
188
+ public
189
+
190
+ # Start the thread pool
191
+ def start
192
+ return self unless @terminate
193
+ @terminate = false
194
+ 0.upto(@num_threads-1) { |i| launch_worker }
195
+ launch_manager
196
+ self
148
197
  end
149
198
 
150
- # Instruct thread pool to terminate
151
- # @param [Boolean] val true/false indicating if thread pool should terminate
152
- def terminate=(val)
153
- @terminate_lock.synchronize { @terminate = val }
199
+ # Return boolean indicating if thread pool is running
200
+ def running?
201
+ !@manager_thread.nil? &&
202
+ ['sleep', 'run'].include?(@manager_thread.status)
154
203
  end
155
204
 
156
205
  # Add work to the pool
157
206
  # @param [ThreadPoolJob] work job to execute in first available thread
207
+ # @return self
158
208
  def <<(work)
209
+ # TODO option to increase worker threads if work queue gets saturated
159
210
  @work_queue.push work
160
- end
161
-
162
- # Return the next job queued up and remove it from the queue
163
- def next_job
164
- @work_queue.pop
211
+ self
165
212
  end
166
213
 
167
214
  # Terminate the thread pool, stopping all worker threads
215
+ #
216
+ # @return self
168
217
  def stop
169
- terminate = true
170
- unless @timout_thread.nil?
171
- @timeout_thread.join
172
- @timeout_thread.terminate
218
+ @terminate = true
219
+
220
+ # this will wake up on it's own, but we can
221
+ # speed things up if we manually wake it up,
222
+ # surround w/ block incase thread cleans up on its own
223
+ begin
224
+ @manager_thread.wakeup if @manager_thread
225
+ rescue
173
226
  end
174
- @timeout_thread = nil
175
- @work_queue.clear
176
- @job_runners_lock.synchronize { @job_runners.each { |jr| jr.stop } }
227
+ self
177
228
  end
178
229
 
179
230
  # Block until all worker threads have finished executing
231
+ #
232
+ # @return self
180
233
  def join
181
- @job_runners_lock.synchronize { @job_runners.each { |jr| jr.join } }
234
+ @manager_thread.join if @manager_thread
235
+ self
182
236
  end
183
237
  end
184
238
 
239
+ end # module RJR
data/site/index.html ADDED
@@ -0,0 +1,45 @@
1
+ <!-- An example rjr js client.
2
+ To run:
3
+ - start bin/rjr-server
4
+ - setup a frontend webserver,
5
+ for example, add the following apache conf:
6
+ Alias /rjr-test /path/to/rjr/site
7
+ ProxyPass /rjr http://localhost:8888
8
+ ProxyPassReverse /rjr http://localhost:8888
9
+ - startup your webserver:
10
+ sudo service httpd start
11
+ (make sure to configure selinux / resolve permissions / etc)
12
+ - navigate to http://localhost/rjr-test
13
+
14
+ Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
15
+ Licensed under the Apache License, Version 2.0
16
+ -->
17
+ <html>
18
+ <head>
19
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
20
+ <script type="text/javascript" src="json.js"></script>
21
+ <script type="text/javascript" src="jrw.js"></script>
22
+ <script type="text/javascript">
23
+ var ws_node = new WSNode('127.0.0.1', '8080');
24
+ ws_node.onopen = function(){
25
+ ws_node.invoke('stress', 'ws1', function(res){
26
+ alert(res.result);
27
+ });
28
+
29
+ ws_node.invoke('failed', 'ws2', function(res){
30
+ alert(res.error.message);
31
+ });
32
+ ws_node.invoke('stress_callback', 3);
33
+ };
34
+ ws_node.open();
35
+
36
+ var www_node = new WebNode('http://localhost/rjr');
37
+ www_node.invoke('stress', 'http1', function(res){
38
+ alert(res.result);
39
+ })
40
+ www_node.invoke('failed', 'http2', function(res){
41
+ alert(res.error.message);
42
+ })
43
+ </script>
44
+ </head>
45
+ </html>