queue_classic 1.0.0 → 1.0.1

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.
data/lib/queue_classic.rb CHANGED
@@ -1,17 +1,17 @@
1
- require 'pg'
1
+ require "pg"
2
2
 
3
- require 'logger'
4
- require 'json'
5
- require 'uri'
3
+ require "logger"
4
+ require "uri"
6
5
 
7
- $: << File.expand_path(__FILE__, 'lib')
6
+ $: << File.expand_path(__FILE__, "lib")
8
7
 
9
- require 'queue_classic/durable_array'
10
- require 'queue_classic/database'
11
- require 'queue_classic/worker'
12
- require 'queue_classic/logger'
13
- require 'queue_classic/queue'
14
- require 'queue_classic/job'
8
+ require "queue_classic/okjson"
9
+ require "queue_classic/durable_array"
10
+ require "queue_classic/database"
11
+ require "queue_classic/worker"
12
+ require "queue_classic/logger"
13
+ require "queue_classic/queue"
14
+ require "queue_classic/job"
15
15
 
16
16
  module QC
17
17
  VERBOSE = ENV["VERBOSE"] || ENV["QC_VERBOSE"]
@@ -3,39 +3,47 @@ module QC
3
3
 
4
4
  @@connection = nil
5
5
 
6
- DATABASE_URL = (ENV["QC_DATABASE_URL"] || ENV["DATABASE_URL"])
7
- MAX_TOP_BOUND = (ENV["QC_TOP_BOUND"] || 9).to_i
8
- NOTIFY_TIMEOUT = (ENV["QC_NOTIFY_TIMEOUT"] || 10).to_i
9
- DEFAULT_QUEUE_NAME = "queue_classic_jobs"
10
-
11
6
  attr_reader :table_name
7
+ attr_reader :top_boundary
12
8
 
13
9
  def initialize(queue_name=nil)
14
10
  log("initialized")
15
11
 
16
- @top_boundry = MAX_TOP_BOUND
17
- log("top_boundry=#{@top_boundry}")
12
+ @top_boundary = (ENV["QC_TOP_BOUND"] || 9).to_i
13
+ log("top_boundary=#{@top_boundary}")
18
14
 
19
- @table_name = queue_name || DEFAULT_QUEUE_NAME
15
+ @table_name = queue_name || "queue_classic_jobs"
20
16
  log("table_name=#{@table_name}")
21
17
 
22
- @db_params = URI.parse(DATABASE_URL)
23
- log("uri=#{DATABASE_URL}")
18
+ @channel_name = @table_name
19
+ log("channel_name=#{@channel_name}")
20
+
21
+ db_url = (ENV["QC_DATABASE_URL"] || ENV["DATABASE_URL"])
22
+ @db_params = URI.parse(db_url)
23
+ log("uri=#{db_url}")
24
+ end
25
+
26
+ def set_application_name
27
+ execute("SET application_name = 'queue_classic'")
28
+ end
29
+
30
+ def escape(string)
31
+ connection.escape(string)
24
32
  end
25
33
 
26
34
  def notify
27
35
  log("NOTIFY")
28
- execute("NOTIFY queue_classic_jobs")
36
+ execute("NOTIFY #{@channel_name}")
29
37
  end
30
38
 
31
39
  def listen
32
40
  log("LISTEN")
33
- execute("LISTEN queue_classic_jobs")
41
+ execute("LISTEN #{@channel_name}")
34
42
  end
35
43
 
36
44
  def unlisten
37
45
  log("UNLISTEN")
38
- execute("UNLISTEN queue_classic_jobs")
46
+ execute("UNLISTEN #{@channel_name}")
39
47
  end
40
48
 
41
49
  def drain_notify
@@ -50,12 +58,29 @@ module QC
50
58
  log("done waiting for notify")
51
59
  end
52
60
 
53
- def execute(sql)
54
- log("executing=#{sql}")
61
+ def transaction
62
+ begin
63
+ execute 'BEGIN'
64
+ yield
65
+ execute 'COMMIT'
66
+ rescue Exception
67
+ execute 'ROLLBACK'
68
+ raise
69
+ end
70
+ end
71
+
72
+ def transaction_idle?
73
+ connection.transaction_status == PGconn::PQTRANS_IDLE
74
+ end
75
+
76
+ def execute(sql, *params)
77
+ log("executing #{sql.inspect}, #{params.inspect}")
55
78
  begin
56
- connection.exec(sql)
79
+ params = nil if params.empty?
80
+ connection.exec(sql, params)
57
81
  rescue PGError => e
58
82
  log("execute exception=#{e.inspect}")
83
+ raise
59
84
  end
60
85
  end
61
86
 
@@ -81,8 +106,6 @@ module QC
81
106
  if conn.status != PGconn::CONNECTION_OK
82
107
  log("connection error=#{conn.error}")
83
108
  end
84
- log("setting application name to queue_classic")
85
- conn.exec("SET application_name = 'queue_classic'")
86
109
  conn
87
110
  end
88
111
 
@@ -93,7 +116,7 @@ module QC
93
116
  -- have identical columns to queue_classic_jobs.
94
117
  -- When QC supports queues with columns other than the default, we will have to change this.
95
118
 
96
- CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$
119
+ CREATE OR REPLACE FUNCTION lock_head(tname name, top_boundary integer) RETURNS SETOF queue_classic_jobs AS $$
97
120
  DECLARE
98
121
  unlocked integer;
99
122
  relative_top integer;
@@ -103,20 +126,24 @@ module QC
103
126
  -- The select count(*) is going to slow down dequeue performance but allow
104
127
  -- for more workers. Would love to see some optimization here...
105
128
 
106
- SELECT TRUNC(random() * #{@top_boundry} + 1) INTO relative_top;
107
- EXECUTE 'SELECT count(*) FROM' || tname || '' INTO job_count;
108
- IF job_count < 10 THEN
129
+ EXECUTE 'SELECT count(*) FROM ' ||
130
+ '(SELECT * FROM ' || quote_ident(tname) ||
131
+ ' LIMIT ' || quote_literal(top_boundary) || ') limited'
132
+ INTO job_count;
133
+
134
+ SELECT TRUNC(random() * top_boundary + 1) INTO relative_top;
135
+ IF job_count < top_boundary THEN
109
136
  relative_top = 0;
110
137
  END IF;
111
138
 
112
139
  LOOP
113
140
  BEGIN
114
141
  EXECUTE 'SELECT id FROM '
115
- || tname::regclass
142
+ || quote_ident(tname)
116
143
  || ' WHERE locked_at IS NULL'
117
144
  || ' ORDER BY id ASC'
118
145
  || ' LIMIT 1'
119
- || ' OFFSET ' || relative_top
146
+ || ' OFFSET ' || quote_literal(relative_top)
120
147
  || ' FOR UPDATE NOWAIT'
121
148
  INTO unlocked;
122
149
  EXIT;
@@ -127,7 +154,7 @@ module QC
127
154
  END LOOP;
128
155
 
129
156
  RETURN QUERY EXECUTE 'UPDATE '
130
- || tname::regclass
157
+ || quote_ident(tname)
131
158
  || ' SET locked_at = (CURRENT_TIMESTAMP)'
132
159
  || ' WHERE id = $1'
133
160
  || ' AND locked_at is NULL'
@@ -137,6 +164,19 @@ module QC
137
164
  RETURN;
138
165
  END;
139
166
  $$ LANGUAGE plpgsql;
167
+
168
+ CREATE OR REPLACE FUNCTION lock_head(tname varchar) RETURNS SETOF queue_classic_jobs AS $$
169
+ BEGIN
170
+ RETURN QUERY EXECUTE 'SELECT * FROM lock_head($1,10)' USING tname;
171
+ END;
172
+ $$ LANGUAGE plpgsql;
173
+ EOD
174
+ end
175
+
176
+ def unload_functions
177
+ execute(<<-EOD)
178
+ DROP FUNCTION IF EXISTS lock_head(tname varchar);
179
+ DROP FUNCTION IF EXISTS lock_head(tname name, top_boundary integer);
140
180
  EOD
141
181
  end
142
182
 
@@ -4,10 +4,11 @@ module QC
4
4
  def initialize(database)
5
5
  @database = database
6
6
  @table_name = @database.table_name
7
+ @top_boundary = @database.top_boundary
7
8
  end
8
9
 
9
10
  def <<(details)
10
- execute("INSERT INTO #{@table_name} (details) VALUES ('#{JSON.dump(details)}')")
11
+ execute("INSERT INTO #{@table_name} (details) VALUES ($1)", OkJson.encode(details))
11
12
  @database.notify if ENV["QC_LISTENING_WORKER"] == "true"
12
13
  end
13
14
 
@@ -16,24 +17,20 @@ module QC
16
17
  end
17
18
 
18
19
  def delete(job)
19
- execute("DELETE FROM #{@table_name} WHERE id = #{job.id}")
20
+ execute("DELETE FROM #{@table_name} WHERE id = $1;", job.id)
20
21
  job
21
22
  end
22
23
 
23
- def find(job)
24
- find_one {"SELECT * FROM #{@table_name} WHERE id = #{job.id}"}
25
- end
26
-
27
24
  def search_details_column(q)
28
- find_many { "SELECT * FROM #{@table_name} WHERE details LIKE '%#{q}%'" }
25
+ find_many { ["SELECT * FROM #{@table_name} WHERE details LIKE $1;", "%#{q}%"] }
29
26
  end
30
27
 
31
28
  def first
32
- find_one { "SELECT * FROM lock_head('#{@table_name}')" }
29
+ find_one { ["SELECT * FROM lock_head($1, $2);", @table_name, @top_boundary] }
33
30
  end
34
31
 
35
32
  def each
36
- execute("SELECT * FROM #{@table_name} ORDER BY id ASC").each do |r|
33
+ execute("SELECT * FROM #{@table_name} ORDER BY id ASC;").each do |r|
37
34
  yield Job.new(r)
38
35
  end
39
36
  end
@@ -43,11 +40,11 @@ module QC
43
40
  end
44
41
 
45
42
  def find_many
46
- execute(yield).map {|r| Job.new(r)}
43
+ execute(*yield).map { |r| Job.new(r) }
47
44
  end
48
45
 
49
- def execute(sql)
50
- @database.execute(sql)
46
+ def execute(sql, *params)
47
+ @database.execute(sql, *params)
51
48
  end
52
49
 
53
50
  end
@@ -4,7 +4,7 @@ module QC
4
4
 
5
5
  def initialize(args={})
6
6
  @id = args["id"]
7
- @details = JSON.parse(args["details"])
7
+ @details = OkJson.decode(args["details"])
8
8
  @locked_at = args["locked_at"]
9
9
  end
10
10
 
@@ -0,0 +1,583 @@
1
+ module QC
2
+ # Copyright 2011 Keith Rarick
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ # See https://github.com/kr/okjson for updates.
23
+
24
+ require 'stringio'
25
+
26
+ # Some parts adapted from
27
+ # http://golang.org/src/pkg/json/decode.go and
28
+ # http://golang.org/src/pkg/utf8/utf8.go
29
+ module OkJson
30
+ extend self
31
+
32
+
33
+ # Decodes a json document in string s and
34
+ # returns the corresponding ruby value.
35
+ # String s must be valid UTF-8. If you have
36
+ # a string in some other encoding, convert
37
+ # it first.
38
+ #
39
+ # String values in the resulting structure
40
+ # will be UTF-8.
41
+ def decode(s)
42
+ ts = lex(s)
43
+ v, ts = textparse(ts)
44
+ if ts.length > 0
45
+ raise Error, 'trailing garbage'
46
+ end
47
+ v
48
+ end
49
+
50
+
51
+ # Parses a "json text" in the sense of RFC 4627.
52
+ # Returns the parsed value and any trailing tokens.
53
+ # Note: this is almost the same as valparse,
54
+ # except that it does not accept atomic values.
55
+ def textparse(ts)
56
+ if ts.length < 0
57
+ raise Error, 'empty'
58
+ end
59
+
60
+ typ, _, val = ts[0]
61
+ case typ
62
+ when '{' then objparse(ts)
63
+ when '[' then arrparse(ts)
64
+ else
65
+ raise Error, "unexpected #{val.inspect}"
66
+ end
67
+ end
68
+
69
+
70
+ # Parses a "value" in the sense of RFC 4627.
71
+ # Returns the parsed value and any trailing tokens.
72
+ def valparse(ts)
73
+ if ts.length < 0
74
+ raise Error, 'empty'
75
+ end
76
+
77
+ typ, _, val = ts[0]
78
+ case typ
79
+ when '{' then objparse(ts)
80
+ when '[' then arrparse(ts)
81
+ when :val,:str then [val, ts[1..-1]]
82
+ else
83
+ raise Error, "unexpected #{val.inspect}"
84
+ end
85
+ end
86
+
87
+
88
+ # Parses an "object" in the sense of RFC 4627.
89
+ # Returns the parsed value and any trailing tokens.
90
+ def objparse(ts)
91
+ ts = eat('{', ts)
92
+ obj = {}
93
+
94
+ if ts[0][0] == '}'
95
+ return obj, ts[1..-1]
96
+ end
97
+
98
+ k, v, ts = pairparse(ts)
99
+ obj[k] = v
100
+
101
+ if ts[0][0] == '}'
102
+ return obj, ts[1..-1]
103
+ end
104
+
105
+ loop do
106
+ ts = eat(',', ts)
107
+
108
+ k, v, ts = pairparse(ts)
109
+ obj[k] = v
110
+
111
+ if ts[0][0] == '}'
112
+ return obj, ts[1..-1]
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ # Parses a "member" in the sense of RFC 4627.
119
+ # Returns the parsed values and any trailing tokens.
120
+ def pairparse(ts)
121
+ (typ, _, k), ts = ts[0], ts[1..-1]
122
+ if typ != :str
123
+ raise Error, "unexpected #{k.inspect}"
124
+ end
125
+ ts = eat(':', ts)
126
+ v, ts = valparse(ts)
127
+ [k, v, ts]
128
+ end
129
+
130
+
131
+ # Parses an "array" in the sense of RFC 4627.
132
+ # Returns the parsed value and any trailing tokens.
133
+ def arrparse(ts)
134
+ ts = eat('[', ts)
135
+ arr = []
136
+
137
+ if ts[0][0] == ']'
138
+ return arr, ts[1..-1]
139
+ end
140
+
141
+ v, ts = valparse(ts)
142
+ arr << v
143
+
144
+ if ts[0][0] == ']'
145
+ return arr, ts[1..-1]
146
+ end
147
+
148
+ loop do
149
+ ts = eat(',', ts)
150
+
151
+ v, ts = valparse(ts)
152
+ arr << v
153
+
154
+ if ts[0][0] == ']'
155
+ return arr, ts[1..-1]
156
+ end
157
+ end
158
+ end
159
+
160
+
161
+ def eat(typ, ts)
162
+ if ts[0][0] != typ
163
+ raise Error, "expected #{typ} (got #{ts[0].inspect})"
164
+ end
165
+ ts[1..-1]
166
+ end
167
+
168
+
169
+ # Scans s and returns a list of json tokens,
170
+ # excluding white space (as defined in RFC 4627).
171
+ def lex(s)
172
+ ts = []
173
+ while s.length > 0
174
+ typ, lexeme, val = tok(s)
175
+ if typ == nil
176
+ raise Error, "invalid character at #{s[0,10].inspect}"
177
+ end
178
+ if typ != :space
179
+ ts << [typ, lexeme, val]
180
+ end
181
+ s = s[lexeme.length..-1]
182
+ end
183
+ ts
184
+ end
185
+
186
+
187
+ # Scans the first token in s and
188
+ # returns a 3-element list, or nil
189
+ # if s does not begin with a valid token.
190
+ #
191
+ # The first list element is one of
192
+ # '{', '}', ':', ',', '[', ']',
193
+ # :val, :str, and :space.
194
+ #
195
+ # The second element is the lexeme.
196
+ #
197
+ # The third element is the value of the
198
+ # token for :val and :str, otherwise
199
+ # it is the lexeme.
200
+ def tok(s)
201
+ case s[0]
202
+ when ?{ then ['{', s[0,1], s[0,1]]
203
+ when ?} then ['}', s[0,1], s[0,1]]
204
+ when ?: then [':', s[0,1], s[0,1]]
205
+ when ?, then [',', s[0,1], s[0,1]]
206
+ when ?[ then ['[', s[0,1], s[0,1]]
207
+ when ?] then [']', s[0,1], s[0,1]]
208
+ when ?n then nulltok(s)
209
+ when ?t then truetok(s)
210
+ when ?f then falsetok(s)
211
+ when ?" then strtok(s)
212
+ when Spc then [:space, s[0,1], s[0,1]]
213
+ when ?\t then [:space, s[0,1], s[0,1]]
214
+ when ?\n then [:space, s[0,1], s[0,1]]
215
+ when ?\r then [:space, s[0,1], s[0,1]]
216
+ else numtok(s)
217
+ end
218
+ end
219
+
220
+
221
+ def nulltok(s); s[0,4] == 'null' && [:val, 'null', nil] end
222
+ def truetok(s); s[0,4] == 'true' && [:val, 'true', true] end
223
+ def falsetok(s); s[0,5] == 'false' && [:val, 'false', false] end
224
+
225
+
226
+ def numtok(s)
227
+ m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
228
+ if m && m.begin(0) == 0
229
+ if m[3] && !m[2]
230
+ [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))]
231
+ elsif m[2]
232
+ [:val, m[0], Float(m[0])]
233
+ else
234
+ [:val, m[0], Integer(m[0])]
235
+ end
236
+ end
237
+ end
238
+
239
+
240
+ def strtok(s)
241
+ m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s)
242
+ if ! m
243
+ raise Error, "invalid string literal at #{abbrev(s)}"
244
+ end
245
+ [:str, m[0], unquote(m[0])]
246
+ end
247
+
248
+
249
+ def abbrev(s)
250
+ t = s[0,10]
251
+ p = t['`']
252
+ t = t[0,p] if p
253
+ t = t + '...' if t.length < s.length
254
+ '`' + t + '`'
255
+ end
256
+
257
+
258
+ # Converts a quoted json string literal q into a UTF-8-encoded string.
259
+ # The rules are different than for Ruby, so we cannot use eval.
260
+ # Unquote will raise an error if q contains control characters.
261
+ def unquote(q)
262
+ q = q[1...-1]
263
+ a = q.dup # allocate a big enough string
264
+ r, w = 0, 0
265
+ while r < q.length
266
+ c = q[r]
267
+ case true
268
+ when c == ?\\
269
+ r += 1
270
+ if r >= q.length
271
+ raise Error, "string literal ends with a \"\\\": \"#{q}\""
272
+ end
273
+
274
+ case q[r]
275
+ when ?",?\\,?/,?'
276
+ a[w] = q[r]
277
+ r += 1
278
+ w += 1
279
+ when ?b,?f,?n,?r,?t
280
+ a[w] = Unesc[q[r]]
281
+ r += 1
282
+ w += 1
283
+ when ?u
284
+ r += 1
285
+ uchar = begin
286
+ hexdec4(q[r,4])
287
+ rescue RuntimeError => e
288
+ raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}"
289
+ end
290
+ r += 4
291
+ if surrogate? uchar
292
+ if q.length >= r+6
293
+ uchar1 = hexdec4(q[r+2,4])
294
+ uchar = subst(uchar, uchar1)
295
+ if uchar != Ucharerr
296
+ # A valid pair; consume.
297
+ r += 6
298
+ end
299
+ end
300
+ end
301
+ w += ucharenc(a, w, uchar)
302
+ else
303
+ raise Error, "invalid escape char #{q[r]} in \"#{q}\""
304
+ end
305
+ when c == ?", c < Spc
306
+ raise Error, "invalid character in string literal \"#{q}\""
307
+ else
308
+ # Copy anything else byte-for-byte.
309
+ # Valid UTF-8 will remain valid UTF-8.
310
+ # Invalid UTF-8 will remain invalid UTF-8.
311
+ a[w] = c
312
+ r += 1
313
+ w += 1
314
+ end
315
+ end
316
+ a[0,w]
317
+ end
318
+
319
+
320
+ # Encodes unicode character u as UTF-8
321
+ # bytes in string a at position i.
322
+ # Returns the number of bytes written.
323
+ def ucharenc(a, i, u)
324
+ case true
325
+ when u <= Uchar1max
326
+ a[i] = (u & 0xff).chr
327
+ 1
328
+ when u <= Uchar2max
329
+ a[i+0] = (Utag2 | ((u>>6)&0xff)).chr
330
+ a[i+1] = (Utagx | (u&Umaskx)).chr
331
+ 2
332
+ when u <= Uchar3max
333
+ a[i+0] = (Utag3 | ((u>>12)&0xff)).chr
334
+ a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr
335
+ a[i+2] = (Utagx | (u&Umaskx)).chr
336
+ 3
337
+ else
338
+ a[i+0] = (Utag4 | ((u>>18)&0xff)).chr
339
+ a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr
340
+ a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr
341
+ a[i+3] = (Utagx | (u&Umaskx)).chr
342
+ 4
343
+ end
344
+ end
345
+
346
+
347
+ def hexdec4(s)
348
+ if s.length != 4
349
+ raise Error, 'short'
350
+ end
351
+ (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3])
352
+ end
353
+
354
+
355
+ def subst(u1, u2)
356
+ if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3
357
+ return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself
358
+ end
359
+ return Ucharerr
360
+ end
361
+
362
+
363
+ def unsubst(u)
364
+ if u < Usurrself || u > Umax || surrogate?(u)
365
+ return Ucharerr, Ucharerr
366
+ end
367
+ u -= Usurrself
368
+ [Usurr1 + ((u>>10)&0x3ff), Usurr2 + (u&0x3ff)]
369
+ end
370
+
371
+
372
+ def surrogate?(u)
373
+ Usurr1 <= u && u < Usurr3
374
+ end
375
+
376
+
377
+ def nibble(c)
378
+ case true
379
+ when ?0 <= c && c <= ?9 then c.ord - ?0.ord
380
+ when ?a <= c && c <= ?z then c.ord - ?a.ord + 10
381
+ when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10
382
+ else
383
+ raise Error, "invalid hex code #{c}"
384
+ end
385
+ end
386
+
387
+
388
+ # Encodes x into a json text. It may contain only
389
+ # Array, Hash, String, Numeric, true, false, nil.
390
+ # (Note, this list excludes Symbol.)
391
+ # X itself must be an Array or a Hash.
392
+ # No other value can be encoded, and an error will
393
+ # be raised if x contains any other value, such as
394
+ # Nan, Infinity, Symbol, and Proc, or if a Hash key
395
+ # is not a String.
396
+ # Strings contained in x must be valid UTF-8.
397
+ def encode(x)
398
+ case x
399
+ when Hash then objenc(x)
400
+ when Array then arrenc(x)
401
+ else
402
+ raise Error, 'root value must be an Array or a Hash'
403
+ end
404
+ end
405
+
406
+
407
+ def valenc(x)
408
+ case x
409
+ when Hash then objenc(x)
410
+ when Array then arrenc(x)
411
+ when String then strenc(x)
412
+ when Numeric then numenc(x)
413
+ when true then "true"
414
+ when false then "false"
415
+ when nil then "null"
416
+ else
417
+ raise Error, "cannot encode #{x.class}: #{x.inspect}"
418
+ end
419
+ end
420
+
421
+
422
+ def objenc(x)
423
+ '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}'
424
+ end
425
+
426
+
427
+ def arrenc(a)
428
+ '[' + a.map{|x| valenc(x)}.join(',') + ']'
429
+ end
430
+
431
+
432
+ def keyenc(k)
433
+ case k
434
+ when String then strenc(k)
435
+ else
436
+ raise Error, "Hash key is not a string: #{k.inspect}"
437
+ end
438
+ end
439
+
440
+
441
+ def strenc(s)
442
+ t = StringIO.new
443
+ t.putc(?")
444
+ r = 0
445
+ while r < s.length
446
+ case s[r]
447
+ when ?" then t.print('\\"')
448
+ when ?\\ then t.print('\\\\')
449
+ when ?\b then t.print('\\b')
450
+ when ?\f then t.print('\\f')
451
+ when ?\n then t.print('\\n')
452
+ when ?\r then t.print('\\r')
453
+ when ?\t then t.print('\\t')
454
+ else
455
+ c = s[r]
456
+ case true
457
+ when Spc <= c && c <= ?~
458
+ t.putc(c)
459
+ when true
460
+ u, size = uchardec(s, r)
461
+ r += size - 1 # we add one more at the bottom of the loop
462
+ if u < 0x10000
463
+ t.print('\\u')
464
+ hexenc4(t, u)
465
+ else
466
+ u1, u2 = unsubst(u)
467
+ t.print('\\u')
468
+ hexenc4(t, u1)
469
+ t.print('\\u')
470
+ hexenc4(t, u2)
471
+ end
472
+ else
473
+ # invalid byte; skip it
474
+ end
475
+ end
476
+ r += 1
477
+ end
478
+ t.putc(?")
479
+ t.string
480
+ end
481
+
482
+
483
+ def hexenc4(t, u)
484
+ t.putc(Hex[(u>>12)&0xf])
485
+ t.putc(Hex[(u>>8)&0xf])
486
+ t.putc(Hex[(u>>4)&0xf])
487
+ t.putc(Hex[u&0xf])
488
+ end
489
+
490
+
491
+ def numenc(x)
492
+ if ((x.nan? || x.infinite?) rescue false)
493
+ raise Error, "Numeric cannot be represented: #{x}"
494
+ end
495
+ "#{x}"
496
+ end
497
+
498
+
499
+ # Decodes unicode character u from UTF-8
500
+ # bytes in string s at position i.
501
+ # Returns u and the number of bytes read.
502
+ def uchardec(s, i)
503
+ n = s.length - i
504
+ return [Ucharerr, 1] if n < 1
505
+
506
+ c0 = s[i].ord
507
+
508
+ # 1-byte, 7-bit sequence?
509
+ if c0 < Utagx
510
+ return [c0, 1]
511
+ end
512
+
513
+ # unexpected continuation byte?
514
+ return [Ucharerr, 1] if c0 < Utag2
515
+
516
+ # need continuation byte
517
+ return [Ucharerr, 1] if n < 2
518
+ c1 = s[i+1].ord
519
+ return [Ucharerr, 1] if c1 < Utagx || Utag2 <= c1
520
+
521
+ # 2-byte, 11-bit sequence?
522
+ if c0 < Utag3
523
+ u = (c0&Umask2)<<6 | (c1&Umaskx)
524
+ return [Ucharerr, 1] if u <= Uchar1max
525
+ return [u, 2]
526
+ end
527
+
528
+ # need second continuation byte
529
+ return [Ucharerr, 1] if n < 3
530
+ c2 = s[i+2].ord
531
+ return [Ucharerr, 1] if c2 < Utagx || Utag2 <= c2
532
+
533
+ # 3-byte, 16-bit sequence?
534
+ if c0 < Utag4
535
+ u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx)
536
+ return [Ucharerr, 1] if u <= Uchar2max
537
+ return [u, 3]
538
+ end
539
+
540
+ # need third continuation byte
541
+ return [Ucharerr, 1] if n < 4
542
+ c3 = s[i+3].ord
543
+ return [Ucharerr, 1] if c3 < Utagx || Utag2 <= c3
544
+
545
+ # 4-byte, 21-bit sequence?
546
+ if c0 < Utag5
547
+ u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx)
548
+ return [Ucharerr, 1] if u <= Uchar3max
549
+ return [u, 4]
550
+ end
551
+
552
+ return [Ucharerr, 1]
553
+ end
554
+
555
+
556
+ class Error < ::StandardError
557
+ end
558
+
559
+
560
+ Utagx = 0x80 # 1000 0000
561
+ Utag2 = 0xc0 # 1100 0000
562
+ Utag3 = 0xe0 # 1110 0000
563
+ Utag4 = 0xf0 # 1111 0000
564
+ Utag5 = 0xF8 # 1111 1000
565
+ Umaskx = 0x3f # 0011 1111
566
+ Umask2 = 0x1f # 0001 1111
567
+ Umask3 = 0x0f # 0000 1111
568
+ Umask4 = 0x07 # 0000 0111
569
+ Uchar1max = (1<<7) - 1
570
+ Uchar2max = (1<<11) - 1
571
+ Uchar3max = (1<<16) - 1
572
+ Ucharerr = 0xFFFD # unicode "replacement char"
573
+ Usurrself = 0x10000
574
+ Usurr1 = 0xd800
575
+ Usurr2 = 0xdc00
576
+ Usurr3 = 0xe000
577
+ Umax = 0x10ffff
578
+
579
+ Spc = ' '[0]
580
+ Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t}
581
+ Hex = '0123456789abcdef'
582
+ end
583
+ end
@@ -2,9 +2,9 @@ module QC
2
2
  module AbstractQueue
3
3
 
4
4
  def enqueue(job,*params)
5
- if job.respond_to?(:details) and job.respond_to?(:params)
6
- job = job.signature
5
+ if job.respond_to?(:signature) and job.respond_to?(:params)
7
6
  params = *job.params
7
+ job = job.signature
8
8
  end
9
9
  array << {"job" => job, "params" => params}
10
10
  end
@@ -39,16 +39,15 @@ module QC
39
39
  extend AbstractQueue
40
40
 
41
41
  def self.array
42
- if defined? @@array
43
- @@array
44
- else
45
- @@database = Database.new
46
- @@array = DurableArray.new(@@database)
47
- end
42
+ default_queue.array
48
43
  end
49
44
 
50
45
  def self.database
51
- @@database
46
+ default_queue.database
47
+ end
48
+
49
+ def self.default_queue
50
+ @queue ||= new(nil)
52
51
  end
53
52
 
54
53
  def initialize(queue_name)
@@ -1,21 +1,23 @@
1
1
  namespace :jobs do
2
2
 
3
- task :work => :environment do
4
- QC::Worker.new.start
5
- end
3
+ desc 'Alias for qc:work'
4
+ task :work => 'qc:work'
6
5
 
7
6
  end
8
7
 
9
8
  namespace :qc do
10
9
 
10
+ desc 'Start a new worker for the (default or QUEUE) queue'
11
11
  task :work => :environment do
12
12
  QC::Worker.new.start
13
13
  end
14
14
 
15
+ desc 'Returns the number of jobs in the (default or QUEUE) queue'
15
16
  task :jobs => :environment do
16
- QC.queue_length
17
+ puts QC::Queue.new(ENV['QUEUE']).length
17
18
  end
18
19
 
20
+ desc 'Ensure the database has the necessary functions for QC'
19
21
  task :load_functions => :environment do
20
22
  db = QC::Database.new
21
23
  db.load_functions
data/readme.md CHANGED
@@ -12,51 +12,98 @@ queue_classic features:
12
12
  * JSON encoding for jobs
13
13
  * Forking workers
14
14
  * Postgres' rock-solid locking mechanism
15
+ * Fuzzy-FIFO support (1)
15
16
  * Long term support
16
17
 
18
+ 1.Theory found here: http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf
19
+
20
+ ## Proven
21
+
22
+ I wrote queue_classic to solve a production problem. My problem was that I needed a
23
+ queueing system that wouldn't fall over should I decide to press it nor should it freak out
24
+ if I attached 100 workers to it. However, my problem didn't warrant adding an additional service.
25
+ I was already using PostgreSQL to manage my application's data, why not use PostgreSQL to pass some messages?
26
+ PostgreSQL was already handling thousands of reads and writes per second anyways. Why not add 35 more
27
+ reads/writes per second to my established performance metric?
28
+
29
+ queue_classic handles over **3,000,000** jobs per day. It does this on Heroku's Ronin Database.
30
+
17
31
  ## Quick Start
18
32
 
19
33
  See doc/installation.md for Rails instructions
20
34
 
21
35
  ```bash
22
- $ createdb queue_classic_test
23
- $ psql queue_classic_test
24
- psql=# CREATE TABLE queue_classic_jobs (id serial, details text, locked_at timestamp);
25
- psql=# CREATE INDEX queue_classic_jobs_id_idx ON queue_classic_jobs (id);
26
- $ export QC_DATABASE_URL="postgres://username:password@localhost/queue_classic_test"
27
- $ gem install queue_classic
28
- $ ruby -r queue_classic -e "QC::Database.new.load_functions"
29
- $ ruby -r queue_classic -e "QC.enqueue("Kernel.puts", "hello world")"
30
- $ ruby -r queue_classic -e "QC::Worker.new.start"
36
+ $ createdb queue_classic_test
37
+ $ psql queue_classic_test
38
+ psql- CREATE TABLE queue_classic_jobs (id serial, details text, locked_at timestamp);
39
+ psql- CREATE INDEX queue_classic_jobs_id_idx ON queue_classic_jobs (id);
40
+ $ export QC_DATABASE_URL="postgres://username:password@localhost/queue_classic_test"
41
+ $ gem install queue_classic
42
+ $ ruby -r queue_classic -e "QC::Database.new.load_functions"
43
+ $ ruby -r queue_classic -e "QC.enqueue('Kernel.puts', 'hello world')"
44
+ $ ruby -r queue_classic -e "QC::Worker.new.start"
45
+ ```
46
+
47
+ ## Configure
48
+
49
+ ```bash
50
+ # Enable logging.
51
+ $VERBOSE
52
+
53
+ # Specifies the database that queue_classic will rely upon.
54
+ $QC_DATABASE_URL || $DATABASE_URL
55
+
56
+ # Fuzzy-FIFO
57
+ # For strict FIFO set to 1. Otherwise, worker will
58
+ # attempt to lock a job in this top region.
59
+ # Default: 9
60
+ $QC_TOP_BOUND
61
+
62
+ # If you want your worker to fork a new
63
+ # child process for each job, set this var to 'true'
64
+ # Default: false
65
+ $QC_FORK_WORKER
66
+
67
+ # The worker uses an exp backoff algorithm
68
+ # if you want high throughput don't use Kernel.sleep
69
+ # use LISTEN/NOTIFY sleep. When set to true, the worker's
70
+ # sleep will be preempted by insertion into the queue.
71
+ # Default: false
72
+ $QC_LISTENING_WORKER
73
+
74
+ # The worker uses an exp backoff algorithm. The base of
75
+ # the exponent is 2. This var determines the max power of the
76
+ # exp.
77
+ # Default: 5 which implies max sleep time of 2^(5-1) => 16 seconds
78
+ $QC_MAX_LOCK_ATTEMPTS
79
+
80
+ # This var is important for consumers of the queue.
81
+ # If you have configured many queues, this var will
82
+ # instruct the worker to bind to a particular queue.
83
+ # Default: queue_classic_jobs --which is the default queue table.
84
+ $QUEUE
31
85
  ```
32
86
 
33
87
  ## Hacking on queue_classic
34
88
 
35
89
  ### Dependencies
36
90
 
37
- * Ruby 1.9.2
91
+ * Ruby 1.9.2 (tests work in 1.8.7 but compatibility is not guaranteed or supported)
38
92
  * Postgres ~> 9.0
39
- * Rubygems: pg ~> 0.11.0
93
+ * Rubygem: pg ~> 0.11.0
40
94
 
41
95
  ### Running Tests
42
96
 
43
97
  ```bash
44
- $ bundle
45
- $ createdb queue_classic_test
46
- $ export QC_DATABASE_URL="postgres://username:pass@localhost/queue_classic_test"
47
- $ rake
98
+ $ bundle
99
+ $ createdb queue_classic_test
100
+ $ export QC_DATABASE_URL="postgres://username:pass@localhost/queue_classic_test"
101
+ $ rake
48
102
  ```
49
103
 
50
- ### Building Documentation
51
-
52
- If you are adding new features, please document them in the doc directory. Also,
53
- once you have the markdown in place, please run: ruby doc/build.rb to make HTML
54
- for the docs.
55
-
56
104
  ## Other Resources
57
105
 
58
- ###[Documentation](https://github.com/ryandotsmith/queue_classic/tree/master/doc)
59
-
60
- ###[Example Rails App](https://github.com/ryandotsmith/queue_classic_example)
61
-
62
- ###[Discussion Group](http://groups.google.com/group/queue_classic "discussion group")
106
+ * [Discussion Group](http://groups.google.com/group/queue_classic "discussion group")
107
+ * [Documentation](https://github.com/ryandotsmith/queue_classic/tree/master/doc)
108
+ * [Example Rails App](https://github.com/ryandotsmith/queue_classic_example)
109
+ * [Slide Deck](http://dl.dropbox.com/u/1579953/talks/queue_classic.pdf)
@@ -25,4 +25,49 @@ context "DatabaseTest" do
25
25
  assert @database.connection.notifies.nil?
26
26
  end
27
27
 
28
+ test "execute should return rows" do
29
+ result = @database.execute 'SELECT 11 foo, 22 bar;'
30
+ assert_equal [{'foo'=>'11', 'bar'=>'22'}], result.to_a
31
+ end
32
+
33
+ test "should raise error on failure" do
34
+ assert_raises PGError do
35
+ @database.execute 'SELECT unknown FROM missing;'
36
+ end
37
+ end
38
+
39
+ test "execute should accept parameters" do
40
+ result = @database.execute 'SELECT $2::int b, $1::int a, $1::int + $2::int c;', 123, '456'
41
+ assert_equal [{"a"=>"123", "b"=>"456", "c"=>"579"}], result.to_a
42
+ end
43
+
44
+ def job_count
45
+ @database.execute('SELECT COUNT(*) FROM queue_classic_jobs')[0].values.first.to_i
46
+ end
47
+
48
+ test "transaction should commit" do
49
+ assert_equal true, @database.transaction_idle?
50
+ assert_equal 0, job_count
51
+ @database.transaction do
52
+ assert_equal false, @database.transaction_idle?
53
+ assert_equal 0, job_count
54
+ @database.execute "INSERT INTO queue_classic_jobs (details) VALUES ('test');"
55
+ assert_equal false, @database.transaction_idle?
56
+ assert_equal 1, job_count
57
+ end
58
+ assert_equal true, @database.transaction_idle?
59
+ assert_equal 1, job_count
60
+ end
61
+
62
+ test "transaction should rollback if there's an error" do
63
+ assert_raises RuntimeError do
64
+ @database.transaction do
65
+ @database.execute "INSERT INTO queue_classic_jobs (details) VALUES ('test');"
66
+ assert_equal 1, job_count
67
+ raise "force rollback"
68
+ end
69
+ end
70
+ assert_equal 0, job_count
71
+ end
72
+
28
73
  end
@@ -29,6 +29,18 @@ context "DurableArray" do
29
29
  assert_equal job, @array.first.details
30
30
  end
31
31
 
32
+ test "passes through strings with quotes" do
33
+ job = {"foo'bar\"baz" => 'abc\\def'}
34
+ @array << job
35
+ assert_equal job, @array.first.details
36
+ end
37
+
38
+ test "passes through newlines" do
39
+ job = {"word" => "line1\nline2\nline3\n"}
40
+ @array << job
41
+ assert_equal job, @array.first.details
42
+ end
43
+
32
44
  test "first returns first job when many are in the array" do
33
45
  @array << {"job" => "one"}
34
46
  @array << {"job" => "two"}
@@ -68,13 +80,13 @@ context "DurableArray" do
68
80
  assert_equal([{"job" => "one"},{"job" => "two"}], results)
69
81
  end
70
82
 
71
- test "seach" do
83
+ test "search" do
72
84
  @array << {"job" => "A.signature"}
73
85
  jobs = @array.search_details_column("A.signature")
74
86
  assert_equal "A.signature", jobs.first.signature
75
87
  end
76
88
 
77
- test "seach when data will not match" do
89
+ test "search when data will not match" do
78
90
  @array << {"job" => "A.signature"}
79
91
  jobs = @array.search_details_column("B.signature")
80
92
  assert_equal [], jobs
data/test/job_test.rb CHANGED
@@ -14,7 +14,7 @@ context "Job" do
14
14
  test "signature returns the class and method" do
15
15
  job = QC::Job.new(
16
16
  "id" => 1,
17
- "details" => {:job => "Class.method", :params => []}.to_json,
17
+ "details" => QC::OkJson.encode({"job" => "Class.method", "params" => []}),
18
18
  "locked_at" => nil
19
19
  )
20
20
  assert_equal "Class.method", job.signature
@@ -23,7 +23,7 @@ context "Job" do
23
23
  test "method returns the class method" do
24
24
  job = QC::Job.new(
25
25
  "id" => 1,
26
- "details" => {:job => "Class.method", :params => []}.to_json,
26
+ "details" => QC::OkJson.encode({"job" => "Class.method", "params" => []}),
27
27
  "locked_at" => nil
28
28
  )
29
29
  assert_equal "method", job.method
@@ -33,7 +33,7 @@ context "Job" do
33
33
  class WhoHa; end
34
34
  job = QC::Job.new(
35
35
  "id" => 1,
36
- "details" => {:job => "WhoHa.method", :params => []}.to_json,
36
+ "details" => QC::OkJson.encode({"job" => "WhoHa.method", "params" => []}),
37
37
  "locked_at" => nil
38
38
  )
39
39
  assert_equal WhoHa, job.klass
@@ -47,7 +47,7 @@ context "Job" do
47
47
 
48
48
  job = QC::Job.new(
49
49
  "id" => 1,
50
- "details" => {:job => "Mod::K.method", :params => []}.to_json,
50
+ "details" => QC::OkJson.encode({"job" => "Mod::K.method", "params" => []}),
51
51
  "locked_at" => nil
52
52
  )
53
53
  assert_equal Mod::K, job.klass
@@ -56,7 +56,7 @@ context "Job" do
56
56
  test "params returns empty array when nil" do
57
57
  job = QC::Job.new(
58
58
  "id" => 1,
59
- "details" => {:job => "Mod::K.method", :params => nil}.to_json,
59
+ "details" => QC::OkJson.encode({"job" => "Mod::K.method", "params" => nil}),
60
60
  "locked_at" => nil
61
61
  )
62
62
  assert_equal [], job.params
@@ -65,7 +65,7 @@ context "Job" do
65
65
  test "params returns 1 items when there is 1 param" do
66
66
  job = QC::Job.new(
67
67
  "id" => 1,
68
- "details" => {:job => "Mod::K.method", :params => ["arg"]}.to_json,
68
+ "details" => QC::OkJson.encode({"job" => "Mod::K.method", "params" => ["arg"]}),
69
69
  "locked_at" => nil
70
70
  )
71
71
  assert_equal "arg", job.params
@@ -74,7 +74,7 @@ context "Job" do
74
74
  test "params retuns many items when there are many params" do
75
75
  job = QC::Job.new(
76
76
  "id" => 1,
77
- "details" => {:job => "Mod::K.method", :params => ["arg","arg"]}.to_json,
77
+ "details" => QC::OkJson.encode({"job" => "Mod::K.method", "params" => ["arg","arg"]}),
78
78
  "locked_at" => nil
79
79
  )
80
80
  assert_equal ["arg","arg"], job.params
data/test/queue_test.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
+ require 'ostruct'
2
3
 
3
4
  context "Queue" do
4
5
 
@@ -9,7 +10,8 @@ context "Queue" do
9
10
  end
10
11
 
11
12
  test "Queue class has a default table name" do
12
- QC::Queue.enqueue("Klass.method")
13
+ default_table_name = QC::Database.new.table_name
14
+ assert_equal default_table_name, QC::Queue.database.table_name
13
15
  end
14
16
 
15
17
  test "Queue class responds to dequeue" do
@@ -58,4 +60,19 @@ context "Queue" do
58
60
  assert_equal 1, @database.execute("SELECT count(*) from pg_stat_activity")[0]["count"].to_i
59
61
  end
60
62
 
63
+ test "Queue class enqueues a job" do
64
+ job = OpenStruct.new :signature => 'Klass.method', :params => ['param']
65
+ QC::Queue.enqueue(job)
66
+ dequeued_job = QC::Queue.dequeue
67
+ assert_equal "Klass.method", dequeued_job.signature
68
+ assert_equal 'param', dequeued_job.params
69
+ end
70
+
71
+ test "Queues have their own array" do
72
+ refute_equal(Class.new(QC::Queue).array, Class.new(QC::Queue).array)
73
+ end
74
+
75
+ test "Queues have their own database" do
76
+ refute_equal(Class.new(QC::Queue).database, Class.new(QC::Queue).database)
77
+ end
61
78
  end
data/test/worker_test.rb CHANGED
@@ -50,7 +50,8 @@ context "Worker" do
50
50
  test "only makes one connection" do
51
51
  QC.enqueue "TestNotifier.deliver", {}
52
52
  @worker.work
53
- assert_equal 1, @database.execute("SELECT count(*) from pg_stat_activity")[0]["count"].to_i
53
+ assert_equal 1, @database.execute("SELECT count(*) from pg_stat_activity")[0]["count"].to_i,
54
+ "Multiple connections -- Are there other connections in other terminals?"
54
55
  end
55
56
 
56
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: queue_classic
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2011-08-22 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pg
16
- requirement: &2153191980 !ruby/object:Gem::Requirement
16
+ requirement: &16027180 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 0.11.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2153191980
24
+ version_requirements: *16027180
25
25
  description: queue_classic is a queueing library for Ruby apps. (Rails, Sinatra, Etc...)
26
26
  queue_classic features asynchronous job polling, database maintained locks and no
27
27
  ridiculous dependencies. As a matter of fact, queue_classic only requires pg.
@@ -31,21 +31,22 @@ extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
33
  - readme.md
34
+ - lib/queue_classic/okjson.rb
35
+ - lib/queue_classic/logger.rb
36
+ - lib/queue_classic/worker.rb
34
37
  - lib/queue_classic/database.rb
35
- - lib/queue_classic/durable_array.rb
36
38
  - lib/queue_classic/job.rb
37
- - lib/queue_classic/logger.rb
38
39
  - lib/queue_classic/queue.rb
39
40
  - lib/queue_classic/tasks.rb
40
- - lib/queue_classic/worker.rb
41
+ - lib/queue_classic/durable_array.rb
41
42
  - lib/queue_classic.rb
42
- - test/database_helpers.rb
43
43
  - test/database_test.rb
44
- - test/durable_array_test.rb
45
- - test/helper.rb
46
44
  - test/job_test.rb
47
- - test/queue_test.rb
45
+ - test/database_helpers.rb
48
46
  - test/worker_test.rb
47
+ - test/queue_test.rb
48
+ - test/durable_array_test.rb
49
+ - test/helper.rb
49
50
  homepage: http://github.com/ryandotsmith/queue_classic
50
51
  licenses: []
51
52
  post_install_message:
@@ -66,13 +67,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
67
  version: '0'
67
68
  requirements: []
68
69
  rubyforge_project:
69
- rubygems_version: 1.8.7
70
+ rubygems_version: 1.8.10
70
71
  signing_key:
71
72
  specification_version: 3
72
73
  summary: postgres backed queue
73
74
  test_files:
74
75
  - test/database_test.rb
75
- - test/durable_array_test.rb
76
76
  - test/job_test.rb
77
- - test/queue_test.rb
78
77
  - test/worker_test.rb
78
+ - test/queue_test.rb
79
+ - test/durable_array_test.rb