disque 0.0.1.alpha → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6753b7f8705cec0eefb2a66dbf7a4fe7db031912
4
- data.tar.gz: b25be6917de9eff8d7a85a955774d4ef91851541
3
+ metadata.gz: a89227c0d69455fc6897e54b94b06239d0e0ad56
4
+ data.tar.gz: a913c4b33ba3bee489f1a87a442675cb0b8d7128
5
5
  SHA512:
6
- metadata.gz: 301fac4892ef2d456611401f668301c7536a94428e0e0e708bdb4671dc236a3c947ce6b091ebcdf167722944bc08b94f6ecdaa372b9ddbec423e3556fc57e19f
7
- data.tar.gz: 01141bcf43fe5c08a4325631d2932766ec71910b006f75d9d3a78ee65139ea7913ec4d6226a4b6d2b01f68b29038ed9ff6895f921ac228c08bf8f7a18c42d669
6
+ metadata.gz: a64e05ec0ffe98863874e148b7ccad5ab34587b5edcf10a5f8dcef1149f67f0a595d96b2f25b61f472e50d53783c80eb66bdc29806280a07b53ac9f42e55a512
7
+ data.tar.gz: ff5b61bd64b033056a42d5f5d162fa7fb8c8e4a57e484f1aa7f91a344ad4a74941a0b8484618b4a8a39d4a47c6875b89c674e46e07667bb6799d5fe62ae8ba87
@@ -0,0 +1 @@
1
+ /tmp
data/AUTHORS ADDED
@@ -0,0 +1,2 @@
1
+ Michel Martens
2
+ Damian Janowski
data/README.md CHANGED
@@ -3,18 +3,39 @@ Disque.rb
3
3
 
4
4
  Client for Disque, an in-memory, distributed job queue.
5
5
 
6
- Status
7
- ------
6
+ Usage
7
+ -----
8
8
 
9
- Disque is expected to be RESP compatible, and that means most Redis
10
- transport clients will be able to connect to it and run any command.
11
- This library is thus an imaginary client for Disque, but one that
12
- should work at least at a very basic level and connect to one node.
9
+ Create a new Disque client by passing a list of nodes:
10
+
11
+ ```ruby
12
+ client = Disque.new(["127.0.0.1:7711", "127.0.0.1:7712", "127.0.0.1:7713"])
13
+ ```
14
+
15
+ Now you can add jobs:
16
+
17
+ ```ruby
18
+ client.push("foo", "bar", 100)
19
+ ```
20
+
21
+ It will push the job "bar" to the queue "foo" with a timeout of 100
22
+ ms, and return the id of the job if it was received and replicated
23
+ in time.
24
+
25
+ Then, your workers will do something like this:
26
+
27
+ ```ruby
28
+ loop do
29
+ client.fetch(from: ["foo"]) do |job|
30
+ # Do something with `job`
31
+ end
32
+ end
33
+ ```
13
34
 
14
35
  Installation
15
36
  ------------
16
37
 
17
- You will be able to install it with RubyGems.
38
+ You can install it using rubygems.
18
39
 
19
40
  ```
20
41
  $ gem install disque
@@ -2,12 +2,12 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "disque"
5
- s.version = "0.0.1.alpha"
5
+ s.version = "0.0.2"
6
6
  s.summary = "Client for Disque"
7
- s.description = "Client for Disque"
8
- s.authors = ["Michel Martens"]
9
- s.email = ["michel@soveran.com"]
10
- s.homepage = "https://github.com/soveran/disque.rb"
7
+ s.description = "Disque for Ruby"
8
+ s.authors = ["Michel Martens", "Damian Janowski"]
9
+ s.email = ["michel@soveran.com", "damian.janowski@gmail.com"]
10
+ s.homepage = "https://github.com/soveran/disque-rb"
11
11
  s.files = `git ls-files`.split("\n")
12
12
  s.license = "MIT"
13
13
 
@@ -1,7 +1,200 @@
1
1
  require "redic"
2
2
 
3
3
  class Disque
4
- def initialize(url)
5
- Redic.new(url)
4
+ ECONN = [
5
+ Errno::ECONNREFUSED,
6
+ Errno::EINVAL,
7
+ ]
8
+
9
+ attr :stats
10
+ attr :nodes
11
+ attr :prefix
12
+
13
+ # Create a new Disque client by passing a list of nodes.
14
+ #
15
+ # Disque.new(["127.0.0.1:7711", "127.0.0.1:7712", "127.0.0.1:7713"])
16
+ #
17
+ # For each operation, a counter is updated to signal which node was
18
+ # the originator of the message. Based on that information, after
19
+ # a full cycle (1000 operations, but configurable on initialization)
20
+ # the stats are checked to see what is the most convenient node
21
+ # to connect to in order to avoid extra jumps.
22
+ #
23
+ # TODO Account for authentication
24
+ # TODO Account for timeout
25
+ def initialize(hosts, cycle: 1000)
26
+
27
+ # Cycle length
28
+ @cycle = cycle
29
+
30
+ # Operations counter
31
+ @count = 0
32
+
33
+ # Known nodes
34
+ @nodes = Hash.new
35
+
36
+ # Connection stats
37
+ @stats = Hash.new(0)
38
+
39
+ # Main client
40
+ @client = Redic.new
41
+
42
+ # Scout client
43
+ @scout = Redic.new
44
+
45
+ # Preferred client prefix
46
+ @prefix = nil
47
+
48
+ explore!(hosts)
49
+ end
50
+
51
+ def url(host)
52
+ sprintf("disque://%s", host)
53
+ end
54
+
55
+ # Collect the list of nodes by means of `CLUSTER NODES` and
56
+ # keep a connection to the node that provided that information.
57
+ def explore!(hosts)
58
+
59
+ # Reset nodes
60
+ @nodes.clear
61
+
62
+ hosts.each do |host|
63
+ begin
64
+ @scout.configure(url(host))
65
+
66
+ @scout.call("CLUSTER", "NODES").lines do |line|
67
+ id, host, flag = line.split
68
+
69
+ prefix = id[0,8]
70
+
71
+ if flag == "myself"
72
+
73
+ # Configure main client
74
+ @client.configure(@scout.url)
75
+
76
+ # Keep track of selected node
77
+ @prefix = prefix
78
+ end
79
+
80
+ @nodes[prefix] = host
81
+ end
82
+
83
+ @scout.quit
84
+
85
+ break
86
+
87
+ rescue *ECONN
88
+ $stderr.puts($!.inspect)
89
+ end
90
+ end
91
+
92
+ if @nodes.empty?
93
+ raise ArgumentError, "nodes unavailable"
94
+ end
95
+ end
96
+
97
+ def pick_client!
98
+ if @count == @cycle
99
+ @count = 0
100
+ prefix, _ = @stats.max { |a, b| a[1] <=> b[1] }
101
+
102
+ if prefix != @prefix
103
+ host = @nodes[prefix]
104
+
105
+ if host
106
+
107
+ # Reconfigure main client
108
+ @client.configure(url(host))
109
+ @prefix = prefix
110
+
111
+ # Reset stats for this new connection
112
+ @stats.clear
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Run commands on the active connection. If the
119
+ # connection is lost, new connections are tried
120
+ # until all nodes become unavailable.
121
+ def call(*args)
122
+ @client.call(*args)
123
+ rescue *ECONN
124
+ explore!(@nodes.values)
125
+ retry
126
+ end
127
+
128
+ # Disque's ADDJOB signature is as follows:
129
+ #
130
+ # ADDJOB queue_name job <ms-timeout>
131
+ # [REPLICATE <count>]
132
+ # [DELAY <sec>]
133
+ # [RETRY <sec>]
134
+ # [TTL <sec>]
135
+ # [MAXLEN <count>]
136
+ # [ASYNC]
137
+ #
138
+ # You can pass any optional arguments as a hash,
139
+ # for example:
140
+ #
141
+ # disque.push("foo", "myjob", 1000, ttl: 1, async: true)
142
+ #
143
+ # Note that `async` is a special case because it's just a
144
+ # flag. That's why `true` must be passed as its value.
145
+ def push(queue_name, job, ms_timeout, options = {})
146
+ command = ["ADDJOB", queue_name, job, ms_timeout]
147
+ command += options_to_arguments(options)
148
+
149
+ call(*command)
150
+ end
151
+
152
+ def fetch(from: [], count: 1, timeout: 0)
153
+ pick_client!
154
+
155
+ jobs = call(
156
+ "GETJOB",
157
+ "TIMEOUT", timeout,
158
+ "COUNT", count,
159
+ "FROM", *from)
160
+
161
+ if jobs then
162
+ @count += 1
163
+
164
+ jobs.each do |queue, msgid, job|
165
+
166
+ # Update stats
167
+ @stats[msgid[2,8]] += 1
168
+
169
+ if block_given?
170
+
171
+ # Process job
172
+ yield(job, queue)
173
+
174
+ # Remove job
175
+ call("ACKJOB", msgid)
176
+ end
177
+ end
178
+ end
179
+
180
+ return jobs
181
+ end
182
+
183
+ def options_to_arguments(options)
184
+ arguments = []
185
+
186
+ options.each do |key, value|
187
+ if value == true
188
+ arguments.push(key)
189
+ else
190
+ arguments.push(key, value)
191
+ end
192
+ end
193
+
194
+ return arguments
195
+ end
196
+
197
+ def quit
198
+ @client.quit
6
199
  end
7
200
  end
data/makefile CHANGED
@@ -1,2 +1,29 @@
1
+ DIR?=./tmp
2
+
3
+ all: start test stop
4
+
1
5
  test:
2
6
  RUBYLIB=./lib cutest tests/*.rb
7
+
8
+ default:
9
+ @echo make \<port\>
10
+ @echo make \[start\|meet\|stop\|list\]
11
+
12
+ start: 7711 7712 7713
13
+ @disque -p 7712 CLUSTER MEET 127.0.0.1 7711 > /dev/null
14
+ @disque -p 7713 CLUSTER MEET 127.0.0.1 7712 > /dev/null
15
+
16
+ stop:
17
+ @kill `cat $(DIR)/disque.*.pid`
18
+
19
+ %:
20
+ @disque-server \
21
+ --port $@ \
22
+ --dir $(DIR) \
23
+ --daemonize yes \
24
+ --bind 127.0.0.1 \
25
+ --loglevel notice \
26
+ --pidfile disque.$@.pid \
27
+ --appendfilename disque.$@.aof \
28
+ --cluster-config-file disque.$@.nodes \
29
+ --logfile disque.$@.log
@@ -0,0 +1,237 @@
1
+ require_relative "../lib/disque"
2
+ require "stringio"
3
+ require "fileutils"
4
+
5
+ module Silencer
6
+ @output = nil
7
+
8
+ def self.start
9
+ $olderr = $stderr
10
+ $stderr = StringIO.new
11
+ end
12
+
13
+ def self.stop
14
+ @output = $stderr.string
15
+ $stderr = $olderr
16
+ end
17
+
18
+ def self.output
19
+ @output
20
+ end
21
+ end
22
+
23
+ DISQUE_NODES = [
24
+ "127.0.0.1:7710",
25
+ "127.0.0.1:7711",
26
+ "127.0.0.1:7712",
27
+ "127.0.0.1:7713",
28
+ ]
29
+
30
+ DISQUE_BAD_NODES = DISQUE_NODES[0,1]
31
+ DISQUE_GOOD_NODES = DISQUE_NODES[1,3]
32
+
33
+ test "raise if connection is not possible" do
34
+ Silencer.start
35
+ assert_raise(ArgumentError) do
36
+ c = Disque.new(DISQUE_BAD_NODES)
37
+ end
38
+ Silencer.stop
39
+
40
+ assert_equal "#<Errno::ECONNREFUSED: Can't connect to: disque://127.0.0.1:7710>\n", Silencer.output
41
+ end
42
+
43
+ test "retry until a connection is reached" do
44
+ Silencer.start
45
+ c = Disque.new(DISQUE_NODES)
46
+ Silencer.stop
47
+
48
+ assert_equal "#<Errno::ECONNREFUSED: Can't connect to: disque://127.0.0.1:7710>\n", Silencer.output
49
+ assert_equal "PONG", c.call("PING")
50
+ end
51
+
52
+ test "lack of jobs" do
53
+ c = Disque.new(DISQUE_GOOD_NODES)
54
+ reached = false
55
+
56
+ c.fetch(from: ["foo"], timeout: 1) do |job|
57
+ reached = true
58
+ end
59
+
60
+ assert_equal false, reached
61
+ end
62
+
63
+ test "one job" do
64
+ c = Disque.new(DISQUE_GOOD_NODES)
65
+
66
+ c.push("foo", "bar", 1000)
67
+
68
+ c.fetch(from: ["foo"], count: 10) do |job, queue|
69
+ assert_equal "bar", job
70
+ end
71
+ end
72
+
73
+ test "multiple jobs" do
74
+ c = Disque.new(DISQUE_GOOD_NODES)
75
+
76
+ c.push("foo", "bar", 1000)
77
+ c.push("foo", "baz", 1000)
78
+
79
+ jobs = ["baz", "bar"]
80
+
81
+ c.fetch(from: ["foo"], count: 10) do |job, queue|
82
+ assert_equal jobs.pop, job
83
+ assert_equal "foo", queue
84
+ end
85
+
86
+ assert jobs.empty?
87
+ end
88
+
89
+ test "multiple queues" do
90
+ c = Disque.new(DISQUE_GOOD_NODES)
91
+
92
+ c.push("foo", "bar", 1000)
93
+ c.push("qux", "baz", 1000)
94
+
95
+ queues = ["qux", "foo"]
96
+ jobs = ["baz", "bar"]
97
+
98
+ result = c.fetch(from: ["foo", "qux"], count: 10) do |job, queue|
99
+ assert_equal jobs.pop, job
100
+ assert_equal queues.pop, queue
101
+ end
102
+
103
+ assert jobs.empty?
104
+ assert queues.empty?
105
+ end
106
+
107
+ test "add jobs with other parameters" do
108
+ c = Disque.new(DISQUE_GOOD_NODES)
109
+
110
+ c.push("foo", "bar", 1000, async: true, ttl: 0)
111
+
112
+ sleep 0.1
113
+
114
+ queues = ["foo"]
115
+ jobs = ["bar"]
116
+
117
+ result = c.fetch(from: ["foo"], count: 10, timeout: 1) do |job, queue|
118
+ assert_equal jobs.pop, job
119
+ assert_equal queues.pop, queue
120
+ end
121
+
122
+ assert_equal ["bar"], jobs
123
+ assert_equal ["foo"], queues
124
+ end
125
+
126
+ test "connect to the best node" do
127
+ c1 = Disque.new([DISQUE_GOOD_NODES[0]], cycle: 2)
128
+ c2 = Disque.new([DISQUE_GOOD_NODES[1]], cycle: 2)
129
+
130
+ assert c1.prefix != c2.prefix
131
+
132
+ # Tamper stats to trigger a reconnection
133
+ c1.stats[c2.prefix] = 10
134
+
135
+ c1.push("q1", "j1", 1000)
136
+ c1.push("q1", "j2", 1000)
137
+
138
+ c2.push("q1", "j3", 1000)
139
+
140
+ c1.fetch(from: ["q1"])
141
+ c1.fetch(from: ["q1"])
142
+ c1.fetch(from: ["q1"])
143
+
144
+ # Client should have reconnected
145
+ assert c1.prefix == c2.prefix
146
+ end
147
+
148
+ test "connect to the best node, part 2" do
149
+ c1 = Disque.new([DISQUE_GOOD_NODES[0]], cycle: 2)
150
+ c2 = Disque.new([DISQUE_GOOD_NODES[1]], cycle: 2)
151
+
152
+ assert c1.prefix != c2.prefix
153
+
154
+ c1.push("q1", "j1", 0)
155
+ c1.push("q1", "j2", 0)
156
+ c1.push("q1", "j3", 0)
157
+
158
+ c2.fetch(from: ["q1"])
159
+ c2.fetch(from: ["q1"])
160
+ c2.fetch(from: ["q1"])
161
+
162
+ # Client should have reconnected
163
+ assert c1.prefix == c2.prefix
164
+ end
165
+
166
+ test "recover after node disconnection" do
167
+ c1 = Disque.new([DISQUE_GOOD_NODES[0]], cycle: 2)
168
+
169
+ prefix = c1.prefix
170
+
171
+ # Tamper stats to trigger a reconnection to a bad node
172
+ c1.stats["fake"] = 10
173
+ c1.nodes["fake"] = DISQUE_BAD_NODES[0]
174
+
175
+ # Delete the other nodes just in case
176
+ c1.nodes.delete_if do |key, val|
177
+ key != prefix &&
178
+ key != "fake"
179
+ end
180
+
181
+ c1.push("q1", "j1", 1000)
182
+ c1.push("q1", "j2", 1000)
183
+ c1.push("q1", "j3", 1000)
184
+
185
+ c1.fetch(from: ["q1"])
186
+ c1.fetch(from: ["q1"])
187
+ c1.fetch(from: ["q1"])
188
+
189
+ # Prefix should stay the same
190
+ assert prefix == c1.prefix
191
+ end
192
+
193
+ test "federation" do
194
+ c1 = Disque.new([DISQUE_GOOD_NODES[0]], cycle: 2)
195
+ c2 = Disque.new([DISQUE_GOOD_NODES[1]], cycle: 2)
196
+
197
+ c1.push("q1", "j1", 0)
198
+
199
+ c2.fetch(from: ["q1"], count: 10) do |job, queue|
200
+ assert_equal "j1", job
201
+ end
202
+ end
203
+
204
+ test "ack jobs when block is given" do
205
+ c = Disque.new(DISQUE_GOOD_NODES)
206
+
207
+ c.push("q1", "j1", 1000)
208
+
209
+ id = nil
210
+
211
+ _, id, _ = c.fetch(from: ["q1"]) { |*a| }[0]
212
+
213
+ assert id
214
+
215
+ info = Hash[*c.call("SHOW", id)]
216
+
217
+ if info.any?
218
+
219
+ # If the test runs too fast, we may get the job
220
+ # with the status set to "acked"
221
+ assert_equal "acked", info.fetch("state")
222
+ end
223
+ end
224
+
225
+ test "don't ack jobs when no block is given" do
226
+ c = Disque.new(DISQUE_GOOD_NODES)
227
+
228
+ c.push("q1", "j1", 1000)
229
+
230
+ _, id, _ = c.fetch(from: ["q1"])[0]
231
+
232
+ assert id
233
+
234
+ info = Hash[*c.call("SHOW", id)]
235
+
236
+ assert_equal info.fetch("state"), "active"
237
+ end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: disque
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.alpha
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michel Martens
8
+ - Damian Janowski
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2015-03-16 00:00:00.000000000 Z
12
+ date: 2015-04-27 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: redic
@@ -24,20 +25,24 @@ dependencies:
24
25
  - - '>='
25
26
  - !ruby/object:Gem::Version
26
27
  version: '0'
27
- description: Client for Disque
28
+ description: Disque for Ruby
28
29
  email:
29
30
  - michel@soveran.com
31
+ - damian.janowski@gmail.com
30
32
  executables: []
31
33
  extensions: []
32
34
  extra_rdoc_files: []
33
35
  files:
34
36
  - .gems
37
+ - .gitignore
38
+ - AUTHORS
35
39
  - LICENSE
36
40
  - README.md
37
41
  - disque.gemspec
38
42
  - lib/disque.rb
39
43
  - makefile
40
- homepage: https://github.com/soveran/disque.rb
44
+ - tests/disque_test.rb
45
+ homepage: https://github.com/soveran/disque-rb
41
46
  licenses:
42
47
  - MIT
43
48
  metadata: {}
@@ -52,12 +57,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
52
57
  version: '0'
53
58
  required_rubygems_version: !ruby/object:Gem::Requirement
54
59
  requirements:
55
- - - '>'
60
+ - - '>='
56
61
  - !ruby/object:Gem::Version
57
- version: 1.3.1
62
+ version: '0'
58
63
  requirements: []
59
64
  rubyforge_project:
60
- rubygems_version: 2.0.14
65
+ rubygems_version: 2.0.3
61
66
  signing_key:
62
67
  specification_version: 4
63
68
  summary: Client for Disque