fiveruns-starling 0.9.7.5

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.
@@ -0,0 +1,62 @@
1
+ # http://en.wikipedia.org/wiki/Thread_pool_pattern
2
+ class ThreadPool
3
+
4
+ # 1. Initialize with arbitrary number of threads
5
+ def initialize( threads_number = 1, &exception_handler )
6
+ @work_queue = SizedQueue.new(threads_number)
7
+ @worker_threads = ThreadGroup.new
8
+
9
+ # Create all threads in advance: that's the thread pool after all
10
+ threads_number.times do
11
+
12
+ worker_thread = Thread.new do
13
+ # Body of the Worker Thread
14
+ begin
15
+ work_to_do = @work_queue.pop() # Blocking
16
+ break if work_to_do == :END_OF_WORK
17
+ begin
18
+ work_to_do.block.call( *work_to_do.args )
19
+ rescue Exception => e # Since they are swallowed by default
20
+ if !exception_handler.nil?
21
+ yield e
22
+ else
23
+ # Standard exception handling
24
+ dump = "Worker Thread thrown an exception:\n"
25
+ dump += "#{e.message}\n(#{e.class.name})\n"
26
+ dump += e.backtrace().join( "\n" ) + "\n"
27
+ print dump
28
+ end
29
+ end
30
+ end until false
31
+ # Worker thread ends up gracefully <img src='http://ruby-rails.pl/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' />
32
+ end
33
+
34
+ @worker_threads.add worker_thread
35
+ end
36
+ end
37
+
38
+ # 2. Add job to the queue, example:
39
+ def add_work( *args, &block )
40
+ @work_queue.push( Runnable.new( args, &block ) )
41
+ end
42
+
43
+ # 3. Allow working threads to end up after finishing all queued work
44
+ def no_more_work
45
+ threads_n = @worker_threads.list.length
46
+ threads_n.times { @work_queue.push :END_OF_WORK }
47
+ end
48
+
49
+ # 4. Wait for all work to finish
50
+ def join
51
+ @worker_threads.list.each { |t| t.join }
52
+ end
53
+
54
+ end
55
+
56
+ class Runnable
57
+ attr_accessor :args, :block
58
+ def initialize( args, &block )
59
+ @args = args
60
+ @block = block
61
+ end
62
+ end
@@ -0,0 +1,239 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'thread'
4
+ require 'timeout'
5
+ require 'starling/thread_pool'
6
+
7
+ module StarlingWorker
8
+ attr_reader :logger
9
+
10
+ VERSION = "0.9.7.5"
11
+
12
+ class Base
13
+ DEFAULT_TIMEOUT = 10
14
+ DEFAULT_CONTINUES_PROCESSING = true
15
+
16
+ # you just need to reimplement this
17
+ def process(message=nil, &block)
18
+ if block
19
+ @block = block
20
+ else
21
+ raise "you need to implement process"
22
+ end
23
+ end
24
+
25
+ def run
26
+ process_worker(&@block)
27
+ end
28
+
29
+ def initialize(opts = {})
30
+ @opts = {
31
+ :timeout => DEFAULT_TIMEOUT,
32
+ :incoming_remote_queue_name => nil,
33
+ :outgoing_remote_queue_name => nil,
34
+ :continues_processing => DEFAULT_CONTINUES_PROCESSING,
35
+ :threads => 10,
36
+ :log_level => Logger::INFO
37
+ }.merge(opts)
38
+
39
+ @@logger = case @opts[:logger]
40
+ when IO, String; Logger.new(@opts[:logger])
41
+ when Logger; @opts[:logger]
42
+ else; Logger.new(STDERR)
43
+ end
44
+
45
+ @@logger = SyslogLogger.new(@opts[:syslog_channel]) if @opts[:syslog_channel]
46
+
47
+ @@logger.level = @opts[:log_level] || Logger::ERROR
48
+
49
+ @@logger.info "StarlingWorker#{self.class.to_s} STARTUP"
50
+
51
+ unless @opts[:host] && @opts[:port]
52
+ raise "you need to pass starling host en port"
53
+ else
54
+ @@logger.info "StarlingWorker#{self.class.to_s} connecting to starling"
55
+ @starling = Starling.new("#{@opts[:host]}:#{@opts[:port]}", :multithread => true)
56
+ end
57
+
58
+ @@logger.info "StarlingWorker#{self.class.to_s} ThreadPool #{@opts[:threads]}"
59
+ @threadpool = ThreadPool.new(@opts[:threads])
60
+
61
+ @@logger.info "StarlingWorker#{self.class.to_s} Created local queue"
62
+ @messages = Queue.new #local queue
63
+ end
64
+
65
+ def process_worker(&block)
66
+ enq_thread = Thread.new do
67
+ @@logger.info "StarlingWorker#{self.class.to_s} starting process_message_from_remote_queue"
68
+ if @opts[:continues_processing]
69
+ while @opts[:continues_processing]
70
+ process_message_from_incoming_remote_queue(&block)
71
+ end
72
+ else
73
+ process_message_from_incoming_remote_queue(&block)
74
+ end
75
+ end
76
+
77
+ enq_thread.join unless @opts[:continues_processing]
78
+
79
+ deq_thread = Thread.new do
80
+ @@logger.info "StarlingWorker#{self.class.to_s} starting process_message_from_local_queue_to_outgoing_remote_queue"
81
+ if @opts[:continues_processing]
82
+ while @opts[:continues_processing]
83
+ process_message_from_local_queue_to_outgoing_remote_queue
84
+ end
85
+ else
86
+ process_message_from_local_queue_to_outgoing_remote_queue
87
+ end
88
+ end if @opts[:outgoing_remote_queue_name]
89
+
90
+ deq_thread.join unless @opts[:continues_processing] || @opts[:outgoing_remote_queue_name] == nil
91
+
92
+ if @opts[:continues_processing]
93
+ enq_thread.join
94
+ deq_thread.join unless @opts[:outgoing_remote_queue_name] == nil
95
+ end
96
+ end
97
+
98
+ def process_message_from_incoming_remote_queue(&block)
99
+ begin
100
+ if @opts[:incoming_remote_queue_name]
101
+ starling = Starling.new("#{@opts[:host]}:#{@opts[:port]}", :multithread => true) unless starling
102
+ message = from_remote_queue_to_process(starling)
103
+ while message == nil
104
+ sleep 0.25
105
+ message = from_remote_queue_to_process(starling)
106
+ end
107
+ @@logger.info "StarlingWorker#{self.class.to_s} processing message (#{message})"
108
+ process_as_thread(message, &block)
109
+ else
110
+ @@logger.info "StarlingWorker#{self.class.to_s} processing without message (#{message})"
111
+ process_as_thread(&block) # just process without message
112
+ end
113
+ rescue Exception => e
114
+ puts "--- process_worker ---"
115
+ puts e
116
+ puts e.backtrace.join("\n")
117
+ sleep 1
118
+ end
119
+ end
120
+
121
+ def process_message_from_local_queue_to_outgoing_remote_queue
122
+ begin
123
+ starling = Starling.new("#{@opts[:host]}:#{@opts[:port]}", :multithread => true) unless starling
124
+
125
+ from_local_queue_to_remote_queue(starling)
126
+ rescue Exception => e
127
+ puts "--- process_worker ---"
128
+ puts e
129
+ puts e.backtrace.join("\n")
130
+ sleep 1
131
+ end
132
+ end
133
+
134
+ def process_as_thread(message=nil, &block)
135
+ @threadpool.add_work do
136
+ timeout(@opts[:timeout]) do
137
+ if block
138
+ if message
139
+ @@logger.info "StarlingWorker#{self.class.to_s} processing block message (#{message})"
140
+ from_process_to_local_queue block.call(message)
141
+ else
142
+ @@logger.info "StarlingWorker#{self.class.to_s} processing block without message (#{message})"
143
+ from_process_to_local_queue block.call
144
+ end
145
+ else
146
+ if message
147
+ @@logger.info "StarlingWorker#{self.class.to_s} processing method message (#{message})"
148
+ from_process_to_local_queue process(message)
149
+ else
150
+ @@logger.info "StarlingWorker#{self.class.to_s} processing method without message (#{message})"
151
+ from_process_to_local_queue process
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def from_remote_queue_to_process(starling=@starling)
159
+ if @opts[:incoming_remote_queue_name]
160
+ message = get_message_from_incoming_remote_queue(starling)
161
+
162
+ while message == nil
163
+ sleep 0.25
164
+ message = get_message_from_incoming_remote_queue(starling)
165
+ end
166
+
167
+ @@logger.info "StarlingWorker#{self.class.to_s} return remote message to process (#{message})"
168
+
169
+ message
170
+ end
171
+ end
172
+
173
+ def from_process_to_local_queue(message)
174
+ @@logger.info "StarlingWorker#{self.class.to_s} add message to local queue (#{message})" if @opts[:outgoing_remote_queue_name]
175
+ add_message_to_local_queue(message) if @opts[:outgoing_remote_queue_name]
176
+ end
177
+
178
+ def from_local_queue_to_remote_queue(starling=@starling)
179
+ if @opts[:outgoing_remote_queue_name]
180
+ message = get_message_from_local_queue
181
+
182
+ @@logger.info "StarlingWorker::#{self.class.to_s} set message on remote queue (#{message})"
183
+
184
+ starling.set(@opts[:outgoing_remote_queue_name], message)
185
+ end
186
+ end
187
+
188
+ def threadpool
189
+ @threadpool
190
+ end
191
+
192
+ def incoming_remote_queue_name
193
+ @opts[:incoming_remote_queue_name]
194
+ end
195
+
196
+ def outgoing_remote_queue_name
197
+ @opts[:outgoing_remote_queue_name]
198
+ end
199
+
200
+ def add_message_to_local_queue(message)
201
+ @messages.enq message
202
+ true
203
+ end
204
+
205
+ def get_message_from_local_queue
206
+ @messages.deq
207
+ end
208
+
209
+ def local_queue
210
+ @messages
211
+ end
212
+
213
+ def add_message_to_incoming_remote_queue(message, starling=@starling)
214
+ starling.set(incoming_remote_queue_name, message)
215
+ true
216
+ end
217
+
218
+ def get_message_from_incoming_remote_queue(starling=@starling)
219
+ starling.get(incoming_remote_queue_name)
220
+ end
221
+
222
+ def add_message_to_outgoing_remote_queue(message, starling=@starling)
223
+ starling.set(outgoing_remote_queue_name, message)
224
+ true
225
+ end
226
+
227
+ def get_message_from_outgoing_remote_queue(starling=@starling)
228
+ starling.get(outgoing_remote_queue_name)
229
+ end
230
+
231
+ def flush_remote_queue
232
+ @starling.flush
233
+ end
234
+
235
+ def self.logger
236
+ @@logger
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,100 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'rubygems'
4
+ require 'spec'
5
+ require 'fileutils'
6
+ require 'memcache'
7
+ require 'digest/md5'
8
+
9
+ require 'starling/server'
10
+ require 'starling/client'
11
+
12
+ class StarlingServer::PersistentQueue
13
+ remove_const :SOFT_LOG_MAX_SIZE
14
+ SOFT_LOG_MAX_SIZE = 16 * 1024 # 16 KB
15
+ end
16
+
17
+ def safely_fork(&block)
18
+ # anti-race juice:
19
+ blocking = true
20
+ Signal.trap("USR1") { blocking = false }
21
+
22
+ pid = Process.fork(&block)
23
+
24
+ while blocking
25
+ sleep 0.1
26
+ end
27
+
28
+ pid
29
+ end
30
+
31
+ describe "StarlingServer" do
32
+ before do
33
+ @tmp_path = File.join(File.dirname(__FILE__), "tmp")
34
+ @templates_path = File.join(File.dirname(__FILE__), "templates")
35
+ @workers_path = File.join(File.dirname(__FILE__), "workers")
36
+
37
+ begin
38
+ Dir::mkdir(@tmp_path)
39
+ rescue Errno::EEXIST
40
+ end
41
+
42
+ @server_pid = safely_fork do
43
+ server = StarlingServer::Base.new(:host => '127.0.0.1',
44
+ :port => 22133,
45
+ :path => @tmp_path,
46
+ :logger => Logger.new(STDERR),
47
+ :log_level => Logger::FATAL)
48
+ Signal.trap("INT") { server.stop }
49
+ Process.kill("USR1", Process.ppid)
50
+ server.run
51
+ end
52
+
53
+ @client = StarlingClient::Base.new(:host => '127.0.0.1',
54
+ :port => 22133,
55
+ :templates_path => @templates_path,
56
+ :workers_path => @workers_path)
57
+ end
58
+
59
+ it "should test if tmp_path exists and is writeable" do
60
+ File.exist?(@tmp_path).should be_true
61
+ File.directory?(@tmp_path).should be_true
62
+ File.writable?(@tmp_path).should be_true
63
+ end
64
+
65
+ it "should test if templates_path exists and is writeable" do
66
+ File.exist?(@templates_path).should be_true
67
+ File.directory?(@templates_path).should be_true
68
+ File.writable?(@templates_path).should be_true
69
+ end
70
+
71
+ it "should test if workers_path exists and is writeable" do
72
+ File.exist?(@workers_path).should be_true
73
+ File.directory?(@workers_path).should be_true
74
+ File.writable?(@workers_path).should be_true
75
+ end
76
+
77
+ it "should load templates" do
78
+ @client.load_templates.should eql(["ActiveRecord", "Basic"])
79
+ end
80
+
81
+ it "should load workers" do
82
+ @client.load_workers.should eql(["GetDataFromSomeApi", "PushDataToSomeApi"])
83
+ end
84
+
85
+ it "should return Starling" do
86
+ @client.starling.should_not == nil
87
+ end
88
+
89
+ it "should run" do
90
+ @client.starling.set("get_data_from_some_api_out", "test")
91
+ @client.starling.set("gettingdata_out", "test")
92
+ @client.run
93
+ end
94
+
95
+ after do
96
+ Process.kill("INT", @server_pid)
97
+ Process.wait(@server_pid)
98
+ FileUtils.rm(Dir.glob(File.join(@tmp_path, '*')))
99
+ end
100
+ end
@@ -0,0 +1,205 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'rubygems'
4
+ require 'fileutils'
5
+ require 'memcache'
6
+ require 'digest/md5'
7
+
8
+ require 'starling/server'
9
+
10
+ class StarlingServer::PersistentQueue
11
+ remove_const :SOFT_LOG_MAX_SIZE
12
+ SOFT_LOG_MAX_SIZE = 16 * 1024 # 16 KB
13
+ end
14
+
15
+ def safely_fork(&block)
16
+ # anti-race juice:
17
+ blocking = true
18
+ Signal.trap("USR1") { blocking = false }
19
+
20
+ pid = Process.fork(&block)
21
+
22
+ while blocking
23
+ sleep 0.1
24
+ end
25
+
26
+ pid
27
+ end
28
+
29
+ describe "StarlingServer" do
30
+ before do
31
+ @tmp_path = File.join(File.dirname(__FILE__), "tmp")
32
+
33
+ begin
34
+ Dir::mkdir(@tmp_path)
35
+ rescue Errno::EEXIST
36
+ end
37
+
38
+ @server_pid = safely_fork do
39
+ server = StarlingServer::Base.new(:host => '127.0.0.1',
40
+ :port => 22133,
41
+ :path => @tmp_path,
42
+ :logger => Logger.new(STDERR),
43
+ :log_level => Logger::FATAL)
44
+ Signal.trap("INT") { server.stop }
45
+ Process.kill("USR1", Process.ppid)
46
+ server.run
47
+ end
48
+
49
+ @client = MemCache.new('127.0.0.1:22133')
50
+ end
51
+
52
+ it "should test if temp_path exists and is writeable" do
53
+ File.exist?(@tmp_path).should be_true
54
+ File.directory?(@tmp_path).should be_true
55
+ File.writable?(@tmp_path).should be_true
56
+ end
57
+
58
+ it "should set and get" do
59
+ v = rand((2**32)-1)
60
+ @client.get('test_set_and_get_one_entry').should be_nil
61
+ @client.set('test_set_and_get_one_entry', v)
62
+ @client.get('test_set_and_get_one_entry').should eql(v)
63
+ end
64
+
65
+
66
+ it "should expire entries" do
67
+ v = rand((2**32)-1)
68
+ @client.get('test_set_with_expiry').should be_nil
69
+ now = Time.now.to_i
70
+ @client.set('test_set_with_expiry', v + 2, now)
71
+ @client.set('test_set_with_expiry', v)
72
+ sleep(1.0)
73
+ @client.get('test_set_with_expiry').should eql(v)
74
+ end
75
+
76
+ it "should have age stat" do
77
+ now = Time.now.to_i
78
+ @client.set('test_age', 'nibbler')
79
+ sleep(1.0)
80
+ @client.get('test_age').should eql('nibbler')
81
+
82
+ stats = @client.stats['127.0.0.1:22133']
83
+ stats.has_key?('queue_test_age_age').should be_true
84
+ (stats['queue_test_age_age'] >= 1000).should be_true
85
+ end
86
+
87
+ it "should rotate log" do
88
+ log_rotation_path = File.join(@tmp_path, 'test_log_rotation')
89
+
90
+ Dir.glob("#{log_rotation_path}*").each do |file|
91
+ File.unlink(file) rescue nil
92
+ end
93
+ @client.get('test_log_rotation').should be_nil
94
+
95
+ v = 'x' * 8192
96
+
97
+ @client.set('test_log_rotation', v)
98
+ File.size(log_rotation_path).should eql(8207)
99
+ @client.get('test_log_rotation')
100
+
101
+ @client.get('test_log_rotation').should be_nil
102
+
103
+ @client.set('test_log_rotation', v)
104
+ @client.get('test_log_rotation').should eql(v)
105
+
106
+ File.size(log_rotation_path).should eql(1)
107
+ # rotated log should be erased after a successful roll.
108
+ Dir.glob("#{log_rotation_path}*").size.should eql(1)
109
+ end
110
+
111
+ it "should output statistics per server" do
112
+ stats = @client.stats
113
+ assert_kind_of Hash, stats
114
+ assert stats.has_key?('127.0.0.1:22133')
115
+
116
+ server_stats = stats['127.0.0.1:22133']
117
+
118
+ basic_stats = %w( bytes pid time limit_maxbytes cmd_get version
119
+ bytes_written cmd_set get_misses total_connections
120
+ curr_connections curr_items uptime get_hits total_items
121
+ rusage_system rusage_user bytes_read )
122
+
123
+ basic_stats.each do |stat|
124
+ server_stats.has_key?(stat).should be_true
125
+ end
126
+ end
127
+
128
+ it "should return valid response with unkown command" do
129
+ response = @client.add('blah', 1)
130
+ response.should eql("CLIENT_ERROR bad command line format\r\n")
131
+ end
132
+
133
+ it "should disconnect and reconnect again" do
134
+ v = rand(2**32-1)
135
+ @client.set('test_that_disconnecting_and_reconnecting_works', v)
136
+ @client.reset
137
+ @client.get('test_that_disconnecting_and_reconnecting_works').should eql(v)
138
+ end
139
+
140
+ it "should use epoll on linux" do
141
+ # this may take a few seconds.
142
+ # the point is to make sure that we're using epoll on Linux, so we can
143
+ # handle more than 1024 connections.
144
+
145
+ unless IO::popen("uname").read.chomp == "Linux"
146
+ raise "(Skipping epoll test: not on Linux)"
147
+ skip = true
148
+ end
149
+ fd_limit = IO::popen("bash -c 'ulimit -n'").read.chomp.to_i
150
+ unless fd_limit > 1024
151
+ raise "(Skipping epoll test: 'ulimit -n' = #{fd_limit}, need > 1024)"
152
+ skip = true
153
+ end
154
+
155
+ unless skip
156
+ v = rand(2**32 - 1)
157
+ @client.set('test_epoll', v)
158
+
159
+ # we can't open 1024 connections to memcache from within this process,
160
+ # because we will hit ruby's 1024 fd limit ourselves!
161
+ pid1 = safely_fork do
162
+ unused_sockets = []
163
+ 600.times do
164
+ unused_sockets << TCPSocket.new("127.0.0.1", 22133)
165
+ end
166
+ Process.kill("USR1", Process.ppid)
167
+ sleep 90
168
+ end
169
+ pid2 = safely_fork do
170
+ unused_sockets = []
171
+ 600.times do
172
+ unused_sockets << TCPSocket.new("127.0.0.1", 22133)
173
+ end
174
+ Process.kill("USR1", Process.ppid)
175
+ sleep 90
176
+ end
177
+
178
+ begin
179
+ client = MemCache.new('127.0.0.1:22133')
180
+ client.get('test_epoll').should eql(v)
181
+ ensure
182
+ Process.kill("TERM", pid1)
183
+ Process.kill("TERM", pid2)
184
+ end
185
+ end
186
+ end
187
+
188
+ it "should raise error if queue collection is an invalid path" do
189
+ invalid_path = nil
190
+ while invalid_path.nil? || File.exist?(invalid_path)
191
+ invalid_path = File.join('/', Digest::MD5.hexdigest(rand(2**32-1).to_s)[0,8])
192
+ end
193
+
194
+ lambda {
195
+ StarlingServer::QueueCollection.new(invalid_path)
196
+ }.should raise_error(StarlingServer::InaccessibleQueuePath)
197
+ end
198
+
199
+ after do
200
+ Process.kill("INT", @server_pid)
201
+ Process.wait(@server_pid)
202
+ @client.reset
203
+ FileUtils.rm(Dir.glob(File.join(@tmp_path, '*')))
204
+ end
205
+ end