disque 0.0.1.alpha → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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