queue_classic 1.0.2 → 2.0.0rc1

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
@@ -6,18 +6,77 @@ require "uri"
6
6
  $: << File.expand_path(__FILE__, "lib")
7
7
 
8
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"
9
+ require "queue_classic/conn"
10
+ require "queue_classic/queries"
13
11
  require "queue_classic/queue"
14
- require "queue_classic/job"
12
+ require "queue_classic/worker"
15
13
 
16
14
  module QC
17
- VERBOSE = ENV["VERBOSE"] || ENV["QC_VERBOSE"]
18
- Logger.puts("Logging enabled")
15
+
16
+ Root = File.expand_path(File.dirname(__FILE__))
17
+ SqlFunctions = File.join(QC::Root, "/sql/ddl.sql")
18
+ DropSqlFunctions = File.join(QC::Root, "/sql/drop_ddl.sql")
19
+
20
+ Log = Logger.new($stdout)
21
+ Log.level = (ENV["QC_LOG_LEVEL"] || Logger::DEBUG).to_i
22
+ Log.info("program=queue_classic log=true")
23
+
24
+ DB_URL =
25
+ ENV["QC_DATABASE_URL"] ||
26
+ ENV["DATABASE_URL"] ||
27
+ raise(ArgumentError, "missing QC_DATABASE_URL or DATABASE_URL")
28
+
29
+ # You can use the APP_NAME to query for
30
+ # postgres related process information in the
31
+ # pg_stat_activity table. Don't set this unless
32
+ # you are using PostgreSQL > 9.0
33
+ APP_NAME = ENV["QC_APP_NAME"]
34
+
35
+ # Why do you want to change the table name?
36
+ # Just deal with the default OK?
37
+ # If you do want to change this, you will
38
+ # need to update the PL/pgSQL lock_head() function.
39
+ # Come on. Don't do it.... Just stick with the default.
40
+ TABLE_NAME = "queue_classic_jobs"
41
+
42
+ # Each row in the table will have a column that
43
+ # notes the queue. You can point your workers
44
+ # at different queues but only one at a time.
45
+ QUEUE = ENV["QUEUE"] || "default"
46
+
47
+ # Set this to 1 for strict FIFO.
48
+ # There is nothing special about 9....
49
+ TOP_BOUND = (ENV["QC_TOP_BOUND"] || 9).to_i
50
+
51
+ # If you are using PostgreSQL > 9
52
+ # then you will have access to listen/notify with payload.
53
+ # Set this value if you wish to make your worker more efficient.
54
+ LISTENING_WORKER = !ENV["QC_LISTENING_WORKER"].nil?
55
+
56
+ # Set this variable if you wish for
57
+ # the worker to fork a UNIX process for
58
+ # each locked job. Remember to restablish
59
+ # any database connectoins. See the worker
60
+ # for more details.
61
+ FORK_WORKER = !ENV["QC_FORK_WORKER"].nil?
62
+
63
+ # The worker uses an exponential backoff
64
+ # algorithm to lock a job. This value will be used
65
+ # as the max exponent.
66
+ MAX_LOCK_ATTEMPTS = (ENV["QC_MAX_LOCK_ATTEMPTS"] || 5).to_i
67
+
68
+ if APP_NAME
69
+ Conn.execute("SET application_name = '#{APP_NAME}'")
70
+ end
19
71
 
20
72
  def self.method_missing(sym, *args, &block)
21
- Queue.send(sym, *args, &block)
73
+ default_queue.send(sym, *args, &block)
74
+ end
75
+
76
+ def self.default_queue
77
+ @default_queue ||= begin
78
+ Queue.new(QUEUE, LISTENING_WORKER)
79
+ end
22
80
  end
81
+
23
82
  end
@@ -0,0 +1,97 @@
1
+ module QC
2
+ module Conn
3
+ extend self
4
+
5
+ def execute(stmt, *params)
6
+ log("executing #{stmt.inspect}, #{params.inspect}")
7
+ begin
8
+ params = nil if params.empty?
9
+ r = connection.exec(stmt, params)
10
+ result = []
11
+ r.each {|t| result << t}
12
+ result.length > 1 ? result : result.pop
13
+ rescue PGError => e
14
+ log("execute exception=#{e.inspect}")
15
+ raise
16
+ end
17
+ end
18
+
19
+ def notify(chan)
20
+ log("NOTIFY")
21
+ execute("NOTIFY #{chan}")
22
+ end
23
+
24
+ def listen(chan)
25
+ log("LISTEN")
26
+ execute("LISTEN #{chan}")
27
+ end
28
+
29
+ def unlisten(chan)
30
+ log("UNLISTEN")
31
+ execute("UNLISTEN #{chan}")
32
+ end
33
+
34
+ def drain_notify
35
+ until connection.notifies.nil?
36
+ log("draining notifications")
37
+ end
38
+ end
39
+
40
+ def wait_for_notify(t)
41
+ log("waiting for notify timeout=#{t}")
42
+ connection.wait_for_notify(t) do |event, pid, msg|
43
+ log("received notification #{event}")
44
+ end
45
+ log("done waiting for notify")
46
+ end
47
+
48
+ def transaction
49
+ begin
50
+ execute("BEGIN")
51
+ yield
52
+ execute("COMMIT")
53
+ rescue Exception
54
+ execute("ROLLBACK")
55
+ raise
56
+ end
57
+ end
58
+
59
+ def transaction_idle?
60
+ connection.transaction_status == PGconn::PQTRANS_IDLE
61
+ end
62
+
63
+ def connection
64
+ @connection ||= connect
65
+ end
66
+
67
+ def disconnect
68
+ connection.finish
69
+ @connection = nil
70
+ end
71
+
72
+ def connect
73
+ log("establishing connection")
74
+ conn = PGconn.connect(
75
+ db_url.host,
76
+ db_url.port || 5432,
77
+ nil, '', #opts, tty
78
+ db_url.path.gsub("/",""), # database name
79
+ db_url.user,
80
+ db_url.password
81
+ )
82
+ if conn.status != PGconn::CONNECTION_OK
83
+ log("connection error=#{conn.error}")
84
+ end
85
+ conn
86
+ end
87
+
88
+ def db_url
89
+ URI.parse(DB_URL)
90
+ end
91
+
92
+ def log(msg)
93
+ Log.info(msg)
94
+ end
95
+
96
+ end
97
+ end
@@ -1,7 +1,5 @@
1
1
  module QC
2
- # encoding: UTF-8
3
- #
4
- # Copyright 2011, 2012 Keith Rarick
2
+ # Copyright 2011 Keith Rarick
5
3
  #
6
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
7
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -263,12 +261,6 @@ module OkJson
263
261
  def unquote(q)
264
262
  q = q[1...-1]
265
263
  a = q.dup # allocate a big enough string
266
- rubydoesenc = false
267
- # In ruby >= 1.9, a[w] is a codepoint, not a byte.
268
- if a.class.method_defined?(:force_encoding)
269
- a.force_encoding('UTF-8')
270
- rubydoesenc = true
271
- end
272
264
  r, w = 0, 0
273
265
  while r < q.length
274
266
  c = q[r]
@@ -306,12 +298,7 @@ module OkJson
306
298
  end
307
299
  end
308
300
  end
309
- if rubydoesenc
310
- a[w] = '' << uchar
311
- w += 1
312
- else
313
- w += ucharenc(a, w, uchar)
314
- end
301
+ w += ucharenc(a, w, uchar)
315
302
  else
316
303
  raise Error, "invalid escape char #{q[r]} in \"#{q}\""
317
304
  end
@@ -321,8 +308,6 @@ module OkJson
321
308
  # Copy anything else byte-for-byte.
322
309
  # Valid UTF-8 will remain valid UTF-8.
323
310
  # Invalid UTF-8 will remain invalid UTF-8.
324
- # In ruby >= 1.9, c is a codepoint, not a byte,
325
- # in which case this is still what we want.
326
311
  a[w] = c
327
312
  r += 1
328
313
  w += 1
@@ -457,10 +442,6 @@ module OkJson
457
442
  t = StringIO.new
458
443
  t.putc(?")
459
444
  r = 0
460
-
461
- # In ruby >= 1.9, s[r] is a codepoint, not a byte.
462
- rubydoesenc = s.class.method_defined?(:encoding)
463
-
464
445
  while r < s.length
465
446
  case s[r]
466
447
  when ?" then t.print('\\"')
@@ -475,13 +456,21 @@ module OkJson
475
456
  case true
476
457
  when Spc <= c && c <= ?~
477
458
  t.putc(c)
478
- when rubydoesenc
479
- u = c.ord
480
- surrenc(t, u)
481
- else
459
+ when true
482
460
  u, size = uchardec(s, r)
483
461
  r += size - 1 # we add one more at the bottom of the loop
484
- surrenc(t, u)
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
485
474
  end
486
475
  end
487
476
  r += 1
@@ -491,20 +480,6 @@ module OkJson
491
480
  end
492
481
 
493
482
 
494
- def surrenc(t, u)
495
- if u < 0x10000
496
- t.print('\\u')
497
- hexenc4(t, u)
498
- else
499
- u1, u2 = unsubst(u)
500
- t.print('\\u')
501
- hexenc4(t, u1)
502
- t.print('\\u')
503
- hexenc4(t, u2)
504
- end
505
- end
506
-
507
-
508
483
  def hexenc4(t, u)
509
484
  t.putc(Hex[(u>>12)&0xf])
510
485
  t.putc(Hex[(u>>8)&0xf])
@@ -0,0 +1,56 @@
1
+ module QC
2
+ module Queries
3
+ extend self
4
+
5
+ def insert(q_name, method, args, chan=nil)
6
+ s = "INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
7
+ res = Conn.execute(s, q_name, method, OkJson.encode(args))
8
+ Conn.notify(chan) if chan
9
+ end
10
+
11
+ def lock_head(q_name, top_bound)
12
+ s = "SELECT * FROM lock_head($1, $2)"
13
+ if r = Conn.execute(s, q_name, top_bound)
14
+ {
15
+ :id => r["id"],
16
+ :method => r["method"],
17
+ :args => OkJson.decode(r["args"])
18
+ }
19
+ end
20
+ end
21
+
22
+ def count(q_name=nil)
23
+ s = "SELECT COUNT(*) FROM #{TABLE_NAME}"
24
+ s << " WHERE q_name = $1" if q_name
25
+ r = Conn.execute(*[s, q_name].compact)
26
+ r["count"].to_i
27
+ end
28
+
29
+ def delete(id)
30
+ Conn.execute("DELETE FROM #{TABLE_NAME} where id = $1", id)
31
+ end
32
+
33
+ def delete_all(q_name=nil)
34
+ s = "DELETE FROM #{TABLE_NAME}"
35
+ s << "WHERE q_name = $1" if q_name
36
+ Conn.execute(*[s, q_name].compact)
37
+ end
38
+
39
+ def load_functions
40
+ file = File.open(SqlFunctions)
41
+ Conn.transaction do
42
+ Conn.execute(file.read)
43
+ end
44
+ file.close
45
+ end
46
+
47
+ def drop_functions
48
+ file = File.open(DropSqlFunctions)
49
+ Conn.transaction do
50
+ Conn.execute(file.read)
51
+ end
52
+ file.close
53
+ end
54
+
55
+ end
56
+ end
@@ -1,66 +1,31 @@
1
1
  module QC
2
- module AbstractQueue
3
-
4
- def enqueue(job,*params)
5
- if job.respond_to?(:signature) and job.respond_to?(:params)
6
- params = *job.params
7
- job = job.signature
8
- end
9
- array << {"job" => job, "params" => params}
10
- end
11
-
12
- def dequeue
13
- array.first
14
- end
15
-
16
- def query(signature)
17
- array.search_details_column(signature)
18
- end
19
-
20
- def delete(job)
21
- array.delete(job)
22
- end
23
-
24
- def delete_all
25
- array.each {|j| delete(j) }
26
- end
27
-
28
- def length
29
- array.count
30
- end
31
-
32
- end
33
- end
34
-
35
- module QC
36
2
  class Queue
37
3
 
38
- include AbstractQueue
39
- extend AbstractQueue
4
+ attr_reader :name, :chan
40
5
 
41
- def self.array
42
- default_queue.array
6
+ def initialize(name, notify=false)
7
+ @name = name
8
+ @chan = @name if notify
43
9
  end
44
10
 
45
- def self.database
46
- default_queue.database
11
+ def enqueue(method, *args)
12
+ Queries.insert(name, method, args, chan)
47
13
  end
48
14
 
49
- def self.default_queue
50
- @queue ||= new(nil)
15
+ def lock(top_bound=TOP_BOUND)
16
+ Queries.lock_head(name, top_bound)
51
17
  end
52
18
 
53
- def initialize(queue_name)
54
- @database = Database.new(queue_name)
55
- @array = DurableArray.new(@database)
19
+ def delete(id)
20
+ Queries.delete(id)
56
21
  end
57
22
 
58
- def array
59
- @array
23
+ def delete_all(q_name=nil)
24
+ Queries.delete_all(q_name)
60
25
  end
61
26
 
62
- def database
63
- @database
27
+ def count(q_name=nil)
28
+ Queries.count(q_name)
64
29
  end
65
30
 
66
31
  end
@@ -1,27 +1,38 @@
1
1
  namespace :jobs do
2
-
3
- desc 'Alias for qc:work'
4
- task :work => 'qc:work'
5
-
2
+ desc "Alias for qc:work"
3
+ task :work => "qc:work"
6
4
  end
7
5
 
8
6
  namespace :qc do
9
-
10
- desc 'Start a new worker for the (default or QUEUE) queue'
7
+ desc "Start a new worker for the (default or $QUEUE) queue"
11
8
  task :work => :environment do
12
- QC::Worker.new.start
9
+ QC::Worker.new(
10
+ QC::TABLE_NAME,
11
+ QC::TOP_BOUND,
12
+ QC::FORK_WORKER,
13
+ QC::LISTENING_WORKER,
14
+ QC::MAX_LOCK_ATTEMPTS
15
+ ).start
13
16
  end
14
17
 
15
- desc 'Returns the number of jobs in the (default or QUEUE) queue'
16
- task :jobs => :environment do
17
- puts QC::Queue.new(ENV['QUEUE']).length
18
+ desc "Returns the number of jobs in the (default or QUEUE) queue"
19
+ task :length => :environment do
20
+ puts QC::Worker.new(
21
+ QC::TABLE_NAME,
22
+ QC::TOP_BOUND,
23
+ QC::FORK_WORKER,
24
+ QC::LISTENING_WORKER,
25
+ QC::MAX_LOCK_ATTEMPTS
26
+ ).length
18
27
  end
19
28
 
20
- desc 'Ensure the database has the necessary functions for QC'
29
+ desc "Ensure the database has the necessary functions for QC"
21
30
  task :load_functions => :environment do
22
- db = QC::Database.new
23
- db.load_functions
24
- db.disconnect
31
+ QC::Queries.load_functions
25
32
  end
26
33
 
34
+ desc "Remove queue_classic functions from database."
35
+ task :load_functions => :environment do
36
+ QC::Queries.drop_functions
37
+ end
27
38
  end