gemerald_beanstalk 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,27 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - jruby-19mode
7
+ - rbx-19mode
8
+
9
+ before_script:
10
+ - bundle config
11
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || git clone https://github.com/beanstalkd/beaneater.git /tmp/beaneater'
12
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || BUNDLE_GEMFILE=/tmp/beaneater/Gemfile bundle install'
13
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || git clone https://github.com/nesquena/backburner.git /tmp/backburner'
14
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || BUNDLE_GEMFILE=/tmp/backburner/Gemfile bundle install'
15
+
16
+ script:
17
+ - rake test
18
+ - sudo sed -i 's/^\(127.0.0.1.*\)$/\1 coveralls.io/' /etc/hosts
19
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || rake start_gemerald_beanstalk_test_server &'
20
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || cd /tmp/beaneater'
21
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || BUNDLE_GEMFILE=/tmp/beaneater/Gemfile bundle exec rake test:full'
22
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || cd /tmp/backburner'
23
+ - '[ "$TRAVIS_RUBY_VERSION" = "jruby-19mode" ] && (exit 0) || BUNDLE_GEMFILE=/tmp/backburner/Gemfile bundle exec rake'
24
+
25
+ matrix:
26
+ allow_failures:
27
+ - rvm: jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'beanstalk_integration_tests', '~> 0.0.8'
7
+ gem 'coveralls', :require => false
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Freewrite.org
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # GemeraldBeanstalk
2
+ [![Build Status](https://travis-ci.org/gemeraldbeanstalk/gemerald_beanstalk.png?branch=master)](https://travis-ci.org/gemeraldbeanstalk/gemerald_beanstalk)
3
+ [![Coverage Status](https://coveralls.io/repos/gemeraldbeanstalk/gemerald_beanstalk/badge.png)](https://coveralls.io/r/gemeraldbeanstalk/gemerald_beanstalk)
4
+ [![Code Climate](https://codeclimate.com/github/gemeraldbeanstalk/gemerald_beanstalk.png)](https://codeclimate.com/github/gemeraldbeanstalk/gemerald_beanstalk)
5
+ [![Dependency Status](https://gemnasium.com/gemeraldbeanstalk/gemerald_beanstalk.png)](https://gemnasium.com/gemeraldbeanstalk/gemerald_beanstalk)
6
+
7
+
8
+ GemeraldBeanstalk offers a Ruby implementation of beanstalkd for testing and other uses.
9
+
10
+ ## Usage
11
+
12
+ GemeraldBeanstalk should work as a drop in replacement for beanstalkd. You can
13
+ start a server via GemeraldBeanstalk::Server.start:
14
+ ```ruby
15
+
16
+ # Start a GemeraldBeanstalk bound to 0.0.0.0:11300
17
+ GemeraldBeanstalk::Server.start
18
+
19
+ # Customize server binding
20
+ GemeraldBeanstalk::Server.start('192.168.1.10', 11301)
21
+ ```
22
+
23
+ GemeraldBeanstalk::Server.start returns an array containing the Thread the
24
+ server is running in and the server's GemeraldBeanstalk::Beanstalk instance.
25
+
26
+ The internals of GemeraldBeanstalk are undocumented at this point, with the
27
+ expectation being that it should be interacted with strictly via the [beanstalkd
28
+ protocol](https://github.com/kr/beanstalkd/blob/master/doc/protocol.md). This
29
+ will likely change in the future, allowing more programatic access directly to
30
+ the GemeraldBeanstalk::Beanstalk.
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ gem 'gemerald_beanstalk'
37
+
38
+ And then execute:
39
+
40
+ $ bundle
41
+
42
+ Or install it yourself as:
43
+
44
+ $ gem install gemerald_beanstalk
45
+
46
+ ## Unbugs!
47
+ In the process of building GemeraldBeanstalk, a number of bugs and inconsistencies
48
+ with Beanstalkd protocol were discovered. Patches have been submitted to correct
49
+ the various bugs and inconsistencies, but they have not yet been merged into
50
+ beanstalkd.
51
+
52
+ It would be fairly tedious to reproduce the behavior of some of the bugs, and as
53
+ such, GemeraldBeanstalk doesn't suffer from them. This can be troubling when
54
+ you run tests that work against GemeraldBeanstalk, but then fail against an
55
+ actual beanstalkd server. Below are a list of those protocol issues that exist
56
+ with Beanstalk, but not with GemeraldBeanstalk.
57
+ * [Pause tube should check tube name valid](https://github.com/kr/beanstalkd/pull/217)
58
+ * [Can't ignore tube with name 200 chars long](https://github.com/kr/beanstalkd/issues/212)
59
+ * [Use of 200-char tube name causes INTERNAL_ERROR](https://github.com/kr/beanstalkd/issues/211)
60
+
61
+ ## Contributing
62
+
63
+ 1. Fork it
64
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
65
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
66
+ 4. Push to the branch (`git push origin my-new-feature`)
67
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'gemerald_beanstalk'
4
+
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ end
10
+
11
+ task :start_gemerald_beanstalk_test_server do
12
+ Thread.abort_on_exception = true
13
+ server_thread, beanstalk = GemeraldBeanstalk::Server.start(ENV['BIND_ADDRESS'], ENV['PORT'])
14
+ trap("SIGINT") { server_thread.kill }
15
+ puts "GemeraldBeanstalk listening on #{beanstalk.address}"
16
+ server_thread.join
17
+ end
18
+
19
+ task :default => :test
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gemerald_beanstalk/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'gemerald_beanstalk'
8
+ spec.version = GemeraldBeanstalk::VERSION
9
+ spec.authors = ['Freewrite.org']
10
+ spec.email = ['dev@freewrite.org']
11
+ spec.description = %q{RubyGem implementation of beanstalkd}
12
+ spec.summary = %q{Gemerald Beanstalk offers a Ruby implementation of beanstalkd for testing and other uses.}
13
+ spec.homepage = 'https://github.com/gemeraldbeanstalk/gemerald_beanstalk'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'eventmachine'
22
+ spec.add_dependency 'thread_safe'
23
+ spec.add_development_dependency 'bundler', '~> 1.3'
24
+ spec.add_development_dependency 'rake'
25
+ end
@@ -0,0 +1,10 @@
1
+ require 'set'
2
+ require 'gemerald_beanstalk/version'
3
+ require 'thread_safe'
4
+ require 'gemerald_beanstalk/job'
5
+ require 'gemerald_beanstalk/beanstalk'
6
+ require 'gemerald_beanstalk/connection'
7
+ require 'gemerald_beanstalk/command'
8
+ require 'gemerald_beanstalk/jobs'
9
+ require 'gemerald_beanstalk/tube'
10
+ require 'gemerald_beanstalk/server'
@@ -0,0 +1,289 @@
1
+ require 'securerandom'
2
+ require 'socket'
3
+ require 'gemerald_beanstalk/beanstalk_helper'
4
+
5
+ class GemeraldBeanstalk::Beanstalk
6
+
7
+ # contains functionality supporting core commands
8
+ include GemeraldBeanstalk::BeanstalkHelper
9
+
10
+ # Colon-separated host and port of the Server the Beanstalk instance is
11
+ # running on.
12
+ attr_reader :address
13
+
14
+ # The maximum size in bytes that a job can be. Jobs exceeding this threshold
15
+ # will result in JOB_TOO_BIG responses.
16
+ attr_reader :max_job_size
17
+
18
+
19
+ # Initializes a new Beanstalk instance with the given +address+ and
20
+ # +maximum_job_size+. +address+ is a string identifying the host and port
21
+ # that the Beanstalk instance is running on. +maximum_job_size+ is the
22
+ # maximum size in bytes that a job can be. Jobs exceeding this threshold will
23
+ # result in JOB_TOO_BIG responses.
24
+ #
25
+ # beanstalk = GemeraldBeanstalk::Beanstalk.new('localhost:11300')
26
+ def initialize(address, maximum_job_size = 2**16)
27
+ @max_job_size = maximum_job_size
28
+ @address = address
29
+ @connections = ThreadSafe::Array.new
30
+ @delayed = ThreadSafe::Array.new
31
+ @id = SecureRandom.base64(16)
32
+ @jobs = GemeraldBeanstalk::Jobs.new
33
+ @mutex = Mutex.new
34
+ @paused = ThreadSafe::Array.new
35
+ @reserved = ThreadSafe::Cache.new {|reserved, key| reserved[key] = [] }
36
+ @stats = ThreadSafe::Hash.new(0)
37
+ @tubes = ThreadSafe::Cache.new
38
+ @up_at = Time.now.to_f
39
+
40
+ tube('default', :create_if_missing)
41
+ end
42
+
43
+
44
+ protected
45
+
46
+ def bury(connection, job_id, priority, *args)
47
+ adjust_stats_key(:'cmd-bury')
48
+ job = find_job(job_id, :only => JOB_RESERVED_STATES)
49
+ return NOT_FOUND if job.nil? || !job.bury(connection, priority)
50
+
51
+ @reserved[connection].delete(job)
52
+ return BURIED
53
+ end
54
+
55
+
56
+ def delete(connection, job_id = nil, *args)
57
+ adjust_stats_key(:'cmd-delete')
58
+ job = find_job(job_id)
59
+ return NOT_FOUND if job.nil?
60
+
61
+ original_state = job.state
62
+ return NOT_FOUND unless job.delete(connection)
63
+
64
+ tube(job.tube_name).delete(job)
65
+ @jobs[job.id - 1] = nil
66
+ @reserved[connection].delete(job) if JOB_RESERVED_STATES.include?(original_state)
67
+
68
+ return DELETED
69
+ end
70
+
71
+
72
+ def ignore(connection, tube_name)
73
+ adjust_stats_key(:'cmd-ignore')
74
+ return NOT_IGNORED if (watched_count = connection.ignore(tube_name)).nil?
75
+ tube = tube(tube_name)
76
+ tube.ignore unless tube.nil?
77
+ return "WATCHING #{watched_count}\r\n"
78
+ end
79
+
80
+
81
+ def kick(connection, limit, *args)
82
+ adjust_stats_key(:'cmd-kick')
83
+ limit = limit.to_i
84
+ kicked = 0
85
+ JOB_INACTIVE_STATES.each do |job_state|
86
+ # GTE to handle negative limits
87
+ break if kicked >= limit
88
+ until (job = tube(connection.tube_used).next_job(job_state, :peek)).nil?
89
+ kicked += 1 if job.kick
90
+ break if kicked == limit
91
+ end
92
+ break if kicked > 0
93
+ end
94
+
95
+ return "KICKED #{kicked}\r\n"
96
+ end
97
+
98
+
99
+ def kick_job(connection, job_id = nil, *args)
100
+ job = find_job(job_id, :only => JOB_INACTIVE_STATES)
101
+ return (!job.nil? && job.kick) ? KICKED : NOT_FOUND
102
+ end
103
+
104
+
105
+ def list_tubes(connection)
106
+ adjust_stats_key(:'cmd-list-tubes')
107
+ return tube_list(active_tubes.keys)
108
+ end
109
+
110
+
111
+ def list_tube_used(connection)
112
+ adjust_stats_key(:'cmd-list-tube-used')
113
+ return "USING #{connection.tube_used}\r\n"
114
+ end
115
+
116
+
117
+ def list_tubes_watched(connection)
118
+ adjust_stats_key(:'cmd-list-tubes-watched')
119
+ return tube_list(connection.tubes_watched)
120
+ end
121
+
122
+
123
+ def pause_tube(connection, tube_name, delay)
124
+ adjust_stats_key(:'cmd-paue-tube')
125
+ return NOT_FOUND if (tube = tube(tube_name)).nil?
126
+ tube.pause(delay.to_i % 2**32)
127
+ @paused << tube
128
+ return PAUSED
129
+ end
130
+
131
+
132
+ def peek(connection, job_id = nil, *args)
133
+ adjust_stats_key(:'cmd-peek')
134
+ return peek_message(find_job(job_id))
135
+ end
136
+
137
+
138
+ def peek_buried(connection)
139
+ return peek_by_state(connection, :buried)
140
+ end
141
+
142
+
143
+ def peek_delayed(connection)
144
+ return peek_by_state(connection, :delayed)
145
+ end
146
+
147
+
148
+ def peek_ready(connection)
149
+ return peek_by_state(connection, :ready)
150
+ end
151
+
152
+
153
+ def put(connection, priority, delay, ttr, bytes, body)
154
+ adjust_stats_key(:'cmd-put')
155
+ bytes = bytes.to_i
156
+ return JOB_TOO_BIG if bytes > @max_job_size
157
+ return EXPECTED_CRLF if body.slice!(-2, 2) != CRLF || body.length != bytes
158
+
159
+ job = nil
160
+ # Ensure job insertion order and ID
161
+ @mutex.synchronize do
162
+ job = GemeraldBeanstalk::Job.new(self, @jobs.next_id, connection.tube_used, priority, delay, ttr, bytes, body)
163
+ @jobs.enqueue(job)
164
+ tube(connection.tube_used).put(job)
165
+ end
166
+
167
+ # Send async so client doesn't wait while we check if job can be immediately dispatched
168
+ connection.transmit("INSERTED #{job.id}\r\n")
169
+
170
+ connection.producer = true
171
+
172
+ case job.state
173
+ when :ready
174
+ honor_reservations(job)
175
+ when :delayed
176
+ @delayed << job
177
+ end
178
+ return nil
179
+ end
180
+
181
+
182
+ def quit(connection)
183
+ disconnect(connection)
184
+ return nil
185
+ end
186
+
187
+
188
+ def release(connection, job_id, priority, delay)
189
+ adjust_stats_key(:'cmd-release')
190
+ job = find_job(job_id, :only => JOB_RESERVED_STATES)
191
+ return NOT_FOUND if job.nil? || !job.release(connection, priority, delay)
192
+
193
+ @reserved[connection].delete(job)
194
+ @delayed << job if job.delayed?
195
+ return RELEASED
196
+ end
197
+
198
+
199
+ def reserve(connection, *args)
200
+ adjust_stats_key(:'cmd-reserve')
201
+ return BAD_FORMAT unless args.empty?
202
+ reserve_job(connection)
203
+ return nil
204
+ end
205
+
206
+
207
+ def reserve_with_timeout(connection, timeout = 0, *args)
208
+ adjust_stats_key(:'cmd-reserve-with-timeout')
209
+ timeout = timeout.to_i
210
+ return nil if reserve_job(connection, timeout) || timeout != 0
211
+ connection.wait_timed_out
212
+ return TIMED_OUT
213
+ end
214
+
215
+
216
+ def stats(connection)
217
+ adjust_stats_key(:'cmd-stats')
218
+ stats = @jobs.counts_by_state.merge(stats_commands).merge({
219
+ 'job-timeouts' => @stats[:'job-timeouts'],
220
+ 'total-jobs' => @jobs.total_jobs,
221
+ 'max-job-size' => @max_job_size,
222
+ 'current-tubes' => active_tubes.length,
223
+ }).merge(stats_connections).merge({
224
+ 'pid' => Process.pid,
225
+ 'version' => GemeraldBeanstalk::VERSION,
226
+ 'rusage-utime' => 0,
227
+ 'rusage-stime' => 0,
228
+ 'uptime' => uptime,
229
+ 'binlog-oldest-index' => 0,
230
+ 'binlog-current-index' => 0,
231
+ 'binlog-records-migrated' => 0,
232
+ 'binlog-records-written' => 0,
233
+ 'binlog-max-size' => 10485760,
234
+ 'id' => @id,
235
+ 'hostname' => Socket.gethostname,
236
+ })
237
+ return yaml_response(stats.map{|stat, value| "#{stat}: #{value}" })
238
+ end
239
+
240
+
241
+ def stats_job(connection, job_id = nil, *args)
242
+ adjust_stats_key(:'cmd-stats-job')
243
+ job = find_job(job_id)
244
+ return NOT_FOUND if job.nil?
245
+
246
+ return yaml_response(job.stats.map{ |stat, value| "#{stat}: #{value}" })
247
+ end
248
+
249
+
250
+ def stats_tube(connection, tube_name)
251
+ adjust_stats_key(:'cmd-stats-tube')
252
+ return NOT_FOUND if (tube = tube(tube_name)).nil?
253
+
254
+ return yaml_response(tube.stats.map{ |stat, value| "#{stat}: #{value}" })
255
+ end
256
+
257
+
258
+ def touch(connection, job_id = nil, *args)
259
+ adjust_stats_key(:'cmd-touch')
260
+ job = find_job(job_id, :only => JOB_RESERVED_STATES)
261
+ return NOT_FOUND if job.nil? || !job.touch(connection)
262
+
263
+ return TOUCHED
264
+ end
265
+
266
+
267
+ def use(connection, tube_name)
268
+ adjust_stats_key(:'cmd-use')
269
+ tube(connection.tube_used).stop_use
270
+ tube(tube_name, :create_if_missing).use
271
+ connection.use(tube_name)
272
+
273
+ return "USING #{tube_name}\r\n"
274
+ end
275
+
276
+
277
+ def watch(connection, tube_name)
278
+ adjust_stats_key(:'cmd-watch')
279
+ if connection.tubes_watched.include?(tube_name)
280
+ watched_count = connection.tubes_watched.length
281
+ else
282
+ tube(tube_name, :create_if_missing).watch
283
+ watched_count = connection.watch(tube_name)
284
+ end
285
+
286
+ return "WATCHING #{watched_count}\r\n"
287
+ end
288
+
289
+ end