queue_classic 1.0.2 → 2.0.0rc1

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