havanna 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 083b1457c1f1af6aa4327822bbb00daba957324d
4
+ data.tar.gz: c5118e5d44add3623f7457caab9d8d1fda81a399
5
+ SHA512:
6
+ metadata.gz: f0e1927e6176f9fa7bb90be4524a52d136bdf8fb3c7221baadb861221fb0eb03a0030d9e5df429bc8f54baa872d23c1df4a0134fe08278577684d1effba8b78f
7
+ data.tar.gz: 55d2ebf43d9fdfcce39a20fd848c8b72fe3e9393133a8eacf56a003ddc44c87a4ca6f5847b5b495f61a1f6c4f9e57c8b372eeedff89c880439c6680c60893969
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ /.env
2
+ /.gs
3
+ /test/**/*.log
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Damian Janowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ Havanna
2
+ =======
3
+
4
+ Ruby workers with [Disque][disque].
5
+
6
+ Usage
7
+ -----
8
+
9
+ Create a worker:
10
+
11
+ ```ruby
12
+ class Mailer
13
+ def call(item)
14
+ puts "Emailing #{item}..."
15
+
16
+ # Actually do it.
17
+ end
18
+ end
19
+ ```
20
+
21
+ Havanna doesn't perform any kind of magic autodiscovery of
22
+ your workers. Similar to Rack's `config.ru`, Havanna has an
23
+ entry point file where you explicitly declare your workers.
24
+ This would be a valid `Havannafile`:
25
+
26
+ ```ruby
27
+ require "app"
28
+
29
+ Havanna.run(Mailer)
30
+ ```
31
+
32
+ Now on the command line, start your workers:
33
+
34
+ ```
35
+ $ havanna start
36
+ ```
37
+
38
+ In a different window, try queuing a job using Disque's
39
+ built-in client:
40
+
41
+ ```
42
+ $ disque addjob Mailer foo@example.com 5000
43
+ ```
44
+
45
+ Once you're up and running, deploy your workers with `-d`
46
+ for daemonization:
47
+
48
+ ```
49
+ $ havanna start -d
50
+ ```
51
+
52
+ Stop the worker pool by issuing a `stop` command:
53
+
54
+ ```
55
+ $ havanna stop
56
+ ```
57
+
58
+ This will wait for all workers to exit gracefully.
59
+
60
+ For more information, run:
61
+
62
+ ```
63
+ $ havanna -h
64
+ ```
65
+
66
+
67
+ Design notes
68
+ ------------
69
+
70
+ Havanna assumes that your workers perform a fair amount of I/O (probably one
71
+ of the most common reasons to send jobs to the background). We will optimize
72
+ Havanna for this use case.
73
+
74
+ Currently, Havanna runs multiple threads per worker. However, we may `fork(2)`
75
+ if we find that's better for multiple-core utilization under MRI.
76
+
77
+
78
+ Alternatives
79
+ ------------
80
+
81
+ It's very likely that Havanna is not for you. While I use it in production,
82
+ it's small and doesn't do much.
83
+
84
+ These are the alternatives I know of in Rubyland:
85
+
86
+ - [Disc][disc]: By my friend [pote][pote]. It supports more customization of
87
+ workers and queues, takes configuration from environment variables and can
88
+ take advantage of Celluloid if you're using it.
89
+
90
+ - [DisqueJockey][disque_jockey]: I don't know much about this one, but
91
+ apparently it's even more configurable, has a DSL and (naturally) might be
92
+ a better fit if you use/like Rails.
93
+
94
+
95
+ About the name
96
+ --------------
97
+
98
+ Havanna is inspired by [Ost][ost] and [ost(1)][ost-bin]. [soveran][soveran]
99
+ named Ost after a café, and I happened to be sitting at another café when I
100
+ started to work on this library. Its name: [Havanna][havanna].
101
+
102
+ By the way, before becoming a café, Havanna produced the best
103
+ *[alfajores][alfajores]* in Argentina. They only had one store in Mar del
104
+ Plata (~400km away from Buenos Aires), so it became a tradition to bring these
105
+ exquisite *alfajores* when you returned from a trip to the beach. Several
106
+ years later they opened stores in Buenos Aires and elsewhere and became a
107
+ coffee shop.
108
+
109
+
110
+ [alfajores]: https://www.google.com.ar/search?q=alfajor+argentino&tbm=isch
111
+ [disc]: https://github.com/pote/disc
112
+ [disque]: https://github.com/antirez/disque
113
+ [disque_jockey]: https://github.com/DevinRiley/disque_jockey
114
+ [havanna]: https://www.google.com.ar/search?q=havanna+cafe&tbm=isch
115
+ [ost-bin]: https://github.com/djanowski/ost-bin
116
+ [ost]: https://github.com/soveran/ost
117
+ [pote]: https://twitter.com/poteland
118
+ [soveran]: https://twitter.com/soveran
data/bin/havanna ADDED
@@ -0,0 +1,137 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ stop = proc do
4
+ if defined?(Havanna)
5
+ Havanna.stop
6
+ else
7
+ exit 0
8
+ end
9
+ end
10
+
11
+ trap(:INT, &stop)
12
+ trap(:TERM, &stop)
13
+
14
+ usage = <<-EOS
15
+ Usage:
16
+
17
+ havanna start [-r <require>] [-d] [-p <pid-path>] [-l <log-path>]
18
+ havanna stop [-p <pid-path>]
19
+
20
+ EOS
21
+
22
+ require "clap"
23
+ require_relative "../lib/havanna"
24
+
25
+ opts = {
26
+ requires: []
27
+ }
28
+
29
+ command, _ = Clap.run ARGV,
30
+ "-d" => -> {
31
+ opts[:daemonize] = true
32
+ opts[:log_path] = File.expand_path("havanna.log") unless opts.include?(:log_path)
33
+ },
34
+ "-l" => -> path {
35
+ opts[:log_path] = path
36
+ },
37
+ "-p" => -> path {
38
+ opts[:pid_path] = path
39
+ },
40
+ "-s" => -> size {
41
+ opts[:pool_size] = Integer(size)
42
+ },
43
+ "-r" => -> file {
44
+ opts[:requires] << file
45
+ },
46
+ "-v" => -> {
47
+ require_relative "../lib/havanna/version"
48
+
49
+ puts Havanna::VERSION
50
+
51
+ exit 0
52
+ },
53
+ "-h" => -> {
54
+ puts(usage)
55
+ exit 0
56
+ }
57
+
58
+ opts[:pid_path] = File.expand_path("havanna.pid") unless opts.include?(:pid_path)
59
+
60
+ opts[:requires].each do |file|
61
+ require(file)
62
+ end
63
+
64
+ module Havanna
65
+ def self.run(worker)
66
+ workers << worker
67
+ end
68
+
69
+ def self.workers
70
+ @workers ||= []
71
+ end
72
+ end
73
+
74
+ case command
75
+ when "start"
76
+ if opts[:daemonize]
77
+ Process.daemon(true)
78
+
79
+ File.open(opts[:pid_path], File::RDWR|File::EXCL|File::CREAT, 0600) do |io|
80
+ io.write(Process.pid)
81
+ end
82
+
83
+ at_exit do
84
+ File.delete(opts[:pid_path]) if File.exists?(opts[:pid_path])
85
+ end
86
+ end
87
+
88
+ if opts[:log_path]
89
+ $stdout.reopen(opts[:log_path], "a")
90
+ $stderr.reopen(opts[:log_path], "a")
91
+ end
92
+
93
+ load "./Havannafile"
94
+
95
+ opts[:pool_size] = Havanna.workers.size unless opts.include?(:pool_size)
96
+
97
+ threads_per_worker = opts[:pool_size] / Havanna.workers.size
98
+
99
+ if threads_per_worker == 0
100
+ abort("Not enough threads for your workers (found #{Havanna.workers.size} workers).")
101
+ end
102
+
103
+ pool = Havanna.workers.each_with_object([]) do |worker, accum|
104
+ accum.concat(Array.new(threads_per_worker) do
105
+ Thread.new(worker) do |worker|
106
+ Thread.current.abort_on_exception = true
107
+ Havanna.start(worker)
108
+ end
109
+ end)
110
+ end
111
+
112
+ pool.each(&:join)
113
+
114
+ when "stop"
115
+ if File.exist?(opts[:pid_path])
116
+ pid = Integer(File.read(opts[:pid_path]).chomp)
117
+
118
+ running = true
119
+
120
+ Process.kill(:TERM, pid)
121
+
122
+ while running
123
+ begin
124
+ Process.kill(0, pid)
125
+ running = true
126
+ rescue Errno::ESRCH
127
+ running = false
128
+ end
129
+ end
130
+ end
131
+
132
+ else
133
+ $stderr.puts("Unkown command #{command.inspect}.")
134
+ $stderr.puts(usage)
135
+
136
+ exit 2
137
+ end
data/havanna.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ require_relative "lib/havanna"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "havanna"
5
+ s.version = Havanna::VERSION
6
+ s.summary = "Ruby workers for Disque."
7
+ s.authors = ["Damian Janowski"]
8
+ s.email = ["damian.janowski@gmail.com"]
9
+ s.homepage = "https://github.com/djanowski/havanna"
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+
13
+ s.executables << "havanna"
14
+
15
+ s.add_dependency "disque"
16
+ s.add_dependency "clap"
17
+
18
+ s.add_development_dependency "cutest"
19
+ end
data/lib/havanna.rb ADDED
@@ -0,0 +1,38 @@
1
+ require "disque"
2
+
3
+ module Havanna
4
+ VERSION = "1.0.0"
5
+
6
+ def self.connect(*args)
7
+ @connect = args
8
+ @disque = Disque.new(*args)
9
+ end
10
+
11
+ def self.start(worker)
12
+ instance = worker.new
13
+
14
+ begin
15
+ disque = Disque.new(*@connect)
16
+
17
+ printf("Started worker %s\n", worker)
18
+
19
+ loop do
20
+ disque.fetch(from: [worker.name], timeout: 5000) do |job|
21
+ instance.call(job)
22
+ end
23
+
24
+ break if @stop
25
+ end
26
+ ensure
27
+ disque.quit
28
+ end
29
+ end
30
+
31
+ def self.push(*args)
32
+ @disque.push(*args)
33
+ end
34
+
35
+ def self.stop
36
+ @stop = true
37
+ end
38
+ end
data/makefile ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ cutest test/havanna_test.rb
3
+
4
+ .PHONY: test
@@ -0,0 +1,228 @@
1
+ require "cutest"
2
+ require "timeout"
3
+ require "disque"
4
+
5
+ at_exit {
6
+ Process.waitall
7
+ }
8
+
9
+ def wait_for_pid(pid)
10
+ wait_for { !running?(pid) }
11
+ end
12
+
13
+ def wait_for_child(pid)
14
+ Timeout.timeout(5) do
15
+ Process.wait(pid)
16
+ end
17
+ end
18
+
19
+ def wait_for
20
+ Timeout.timeout(10) do
21
+ until value = yield
22
+ sleep(0.1)
23
+ end
24
+
25
+ return value
26
+ end
27
+ end
28
+
29
+ def running?(pid)
30
+ begin
31
+ Process.kill(0, pid)
32
+ true
33
+ rescue Errno::ESRCH
34
+ false
35
+ end
36
+ end
37
+
38
+ def read_pid_file(path)
39
+ wait_for { File.exist?(path) && File.size(path) > 0 }
40
+
41
+ Integer(File.read(path))
42
+ end
43
+
44
+ def root(path)
45
+ File.expand_path("../#{path}", File.dirname(__FILE__))
46
+ end
47
+
48
+ disque = Disque.new("127.0.0.1:7711")
49
+
50
+ prepare do
51
+ disque.call("DEBUG", "FLUSHALL")
52
+ Dir["test/workers/**/*.pid"].each { |file| File.delete(file) }
53
+ end
54
+
55
+ test "start" do
56
+ pid = nil
57
+
58
+ begin
59
+ pid = spawn("#{root("bin/havanna")} start", chdir: "test/workers/echo")
60
+
61
+ disque.push("Echo", 2, 5000)
62
+
63
+ job = wait_for { disque.fetch(from: ["Echo:result"]) }
64
+
65
+ assert_equal "2", job[0][-1]
66
+ ensure
67
+ Process.kill(:INT, pid) if pid
68
+ end
69
+ end
70
+
71
+ test "gracefully handles TERM signals" do
72
+ disque.push("Slow", 3, 5000)
73
+
74
+ begin
75
+ spawn("#{root("bin/havanna")} -d start", chdir: "test/workers/slow")
76
+
77
+ pid = read_pid_file("./test/workers/slow/havanna.pid")
78
+
79
+ assert wait_for { disque.call("QLEN", "Slow:result") == 0 }
80
+ ensure
81
+ Process.kill(:TERM, pid) if pid
82
+ end
83
+
84
+ wait_for_pid(pid)
85
+
86
+ assert_equal "3", disque.fetch(from: ["Slow:result"])[0][-1]
87
+ end
88
+
89
+ test "stop waits for workers to be done" do
90
+ spawn("#{root("bin/havanna")} start -d", chdir: "test/workers/slow")
91
+
92
+ pid = read_pid_file("./test/workers/slow/havanna.pid")
93
+
94
+ stopper = spawn("#{root("bin/havanna")} stop", chdir: "test/workers/slow")
95
+
96
+ # Let the stop command start.
97
+ wait_for { running?(stopper) }
98
+
99
+ # Let the stop command end.
100
+ wait_for_child(stopper)
101
+
102
+ # Immediately after the stop command exits,
103
+ # havanna(1) shouldn't be running and the pid file
104
+ # should be gone.
105
+
106
+ assert !running?(pid)
107
+ assert !File.exist?("./test/workers/slow/havanna.pid")
108
+ end
109
+
110
+ test "use a specific path for the pid file" do
111
+ pid = nil
112
+ pid_path = "./test/workers/echo/foo.pid"
113
+
114
+ begin
115
+ spawn("#{root("bin/havanna")} -d start -p foo.pid", chdir: "test/workers/echo")
116
+
117
+ pid = read_pid_file(pid_path)
118
+
119
+ assert pid
120
+ ensure
121
+ Process.kill(:INT, pid) if pid
122
+ end
123
+
124
+ wait_for_pid(pid)
125
+
126
+ assert !File.exist?(pid_path)
127
+ end
128
+
129
+ test "load Havannafile" do
130
+ pid = nil
131
+
132
+ begin
133
+ pid = spawn("#{root("bin/havanna")} start", chdir: "test/workers/echo")
134
+
135
+ disque.push("Echo", 2, 5000)
136
+
137
+ value = wait_for { disque.fetch(from: ["Echo:result"]) }
138
+
139
+ assert_equal "2", value
140
+ ensure
141
+ Process.kill(:INT, pid) if pid
142
+ end
143
+ end
144
+
145
+ test "redirect stdout and stderr to a log file when daemonizing" do
146
+ pid, detached_pid = nil
147
+
148
+ pid_path = "./test/workers/logger/havanna.pid"
149
+
150
+ log_path = "test/workers/logger/havanna.log"
151
+
152
+ File.delete(log_path) if File.exist?(log_path)
153
+
154
+ begin
155
+ pid = spawn("#{root("bin/havanna")} -d start", chdir: "test/workers/logger")
156
+
157
+ assert wait_for {
158
+ `ps -p #{pid} -o state`.lines.to_a.last[/(\w+)/, 1] == "Z"
159
+ }
160
+
161
+ redis.lpush("Logger", 1)
162
+ ensure
163
+ detached_pid = read_pid_file(pid_path)
164
+
165
+ Process.kill(:INT, pid) if pid
166
+ Process.kill(:INT, detached_pid) if detached_pid
167
+ end
168
+
169
+ wait_for_pid(detached_pid)
170
+
171
+ assert_equal "out: 1\nerr: 1\n", File.read(log_path)
172
+ end
173
+
174
+ test "redirect stdout and stderr to a different log file when daemonizing" do
175
+ pid, detached_pid = nil
176
+
177
+ pid_path = "./test/workers/logger/havanna.pid"
178
+
179
+ log_path = "test/workers/logger/foo.log"
180
+
181
+ File.delete(log_path) if File.exist?(log_path)
182
+
183
+ begin
184
+ pid = spawn("#{root("bin/havanna")} -d -l foo.log start", chdir: "test/workers/logger")
185
+
186
+ assert wait_for {
187
+ `ps -p #{pid} -o state`.lines.to_a.last[/(\w+)/, 1] == "Z"
188
+ }
189
+
190
+ redis.lpush("Logger", 1)
191
+ ensure
192
+ detached_pid = read_pid_file(pid_path)
193
+
194
+ Process.kill(:INT, pid) if pid
195
+ Process.kill(:INT, detached_pid) if detached_pid
196
+ end
197
+
198
+ wait_for_pid(detached_pid)
199
+
200
+ assert_equal "out: 1\nerr: 1\n", File.read(log_path)
201
+ end
202
+
203
+ test "daemonizes" do
204
+ pid, detached_pid = nil
205
+
206
+ pid_path = "./test/workers/echo/havanna.pid"
207
+
208
+ begin
209
+ pid = spawn("#{root("bin/havanna")} -d start", chdir: "test/workers/echo")
210
+
211
+ assert wait_for {
212
+ `ps -p #{pid} -o state`.lines.to_a.last[/(\w+)/, 1] == "Z"
213
+ }
214
+
215
+ detached_pid = read_pid_file(pid_path)
216
+
217
+ ppid = `ps -p #{detached_pid} -o ppid`.lines.to_a.last[/(\d+)/, 1]
218
+
219
+ assert_equal "1", ppid
220
+ ensure
221
+ Process.kill(:INT, pid) if pid
222
+ Process.kill(:INT, detached_pid) if detached_pid
223
+ end
224
+
225
+ wait_for_pid(detached_pid)
226
+
227
+ assert !File.exist?(pid_path)
228
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "echo"
2
+
3
+ Havanna.run(Echo)
@@ -0,0 +1,7 @@
1
+ Havanna.connect("127.0.0.1:7711")
2
+
3
+ class Echo
4
+ def call(job)
5
+ Havanna.push("Echo:result", job, 5000)
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "logger"
2
+
3
+ Havanna.run(Logger)
@@ -0,0 +1,8 @@
1
+ Havanna.connect("127.0.0.1:7711")
2
+
3
+ class Logger
4
+ def call(id)
5
+ $stdout.puts("out: #{id}")
6
+ $stderr.puts("err: #{id}")
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "slow"
2
+
3
+ Havanna.run(Slow)
@@ -0,0 +1,8 @@
1
+ Havanna.connect("127.0.0.1:7711")
2
+
3
+ class Slow
4
+ def call(n)
5
+ sleep(n.to_i)
6
+ Havanna.push("Slow:result", n, 5000)
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: havanna
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Damian Janowski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: disque
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: clap
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: cutest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - damian.janowski@gmail.com
58
+ executables:
59
+ - havanna
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .gitignore
64
+ - LICENSE
65
+ - README.md
66
+ - bin/havanna
67
+ - havanna.gemspec
68
+ - lib/havanna.rb
69
+ - makefile
70
+ - test/havanna_test.rb
71
+ - test/workers/echo/Havannafile
72
+ - test/workers/echo/echo.rb
73
+ - test/workers/logger/Havannafile
74
+ - test/workers/logger/logger.rb
75
+ - test/workers/slow/Havannafile
76
+ - test/workers/slow/slow.rb
77
+ homepage: https://github.com/djanowski/havanna
78
+ licenses: []
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.4.6
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Ruby workers for Disque.
100
+ test_files: []