havanna 1.0.0

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.
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: []