queue_classic 2.0.0rc9 → 2.0.0rc10
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/conn.rb +10 -12
- data/lib/queue_classic/queries.rb +5 -3
- data/lib/queue_classic/scrolls.rb +127 -0
- data/lib/queue_classic/worker.rb +43 -39
- data/lib/queue_classic.rb +20 -11
- data/readme.md +13 -1
- data/test/helper.rb +0 -2
- metadata +4 -3
data/lib/queue_classic/conn.rb
CHANGED
@@ -3,7 +3,7 @@ module QC
|
|
3
3
|
extend self
|
4
4
|
|
5
5
|
def execute(stmt, *params)
|
6
|
-
log("
|
6
|
+
log(:level => :debug, :action => "exec_sql", :sql => stmt.inspect)
|
7
7
|
begin
|
8
8
|
params = nil if params.empty?
|
9
9
|
r = connection.exec(stmt, params)
|
@@ -11,38 +11,36 @@ module QC
|
|
11
11
|
r.each {|t| result << t}
|
12
12
|
result.length > 1 ? result : result.pop
|
13
13
|
rescue PGError => e
|
14
|
-
log(
|
14
|
+
log(:error => e.inspect)
|
15
15
|
raise
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
def notify(chan)
|
20
|
-
log("NOTIFY")
|
20
|
+
log(:level => :debug, :action => "NOTIFY")
|
21
21
|
execute('NOTIFY "' + chan + '"') #quotes matter
|
22
22
|
end
|
23
23
|
|
24
24
|
def listen(chan)
|
25
|
-
log("LISTEN")
|
25
|
+
log(:level => :debug, :action => "LISTEN")
|
26
26
|
execute('LISTEN "' + chan + '"') #quotes matter
|
27
27
|
end
|
28
28
|
|
29
29
|
def unlisten(chan)
|
30
|
-
log("UNLISTEN")
|
30
|
+
log(:level => :debug, :action => "UNLISTEN")
|
31
31
|
execute('UNLISTEN "' + chan + '"') #quotes matter
|
32
32
|
end
|
33
33
|
|
34
34
|
def drain_notify
|
35
35
|
until connection.notifies.nil?
|
36
|
-
log(
|
36
|
+
log(:level => :debug, :action => "drain_notifications")
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
40
|
def wait_for_notify(t)
|
41
|
-
log("waiting for notify timeout=#{t}")
|
42
41
|
connection.wait_for_notify(t) do |event, pid, msg|
|
43
|
-
log(
|
42
|
+
log(:level => :debug, :action => "received_notification")
|
44
43
|
end
|
45
|
-
log("done waiting for notify")
|
46
44
|
end
|
47
45
|
|
48
46
|
def transaction
|
@@ -70,7 +68,7 @@ module QC
|
|
70
68
|
end
|
71
69
|
|
72
70
|
def connect
|
73
|
-
log(
|
71
|
+
log(:level => :debug, :action => "establish_conn")
|
74
72
|
conn = PGconn.connect(
|
75
73
|
db_url.host,
|
76
74
|
db_url.port || 5432,
|
@@ -80,7 +78,7 @@ module QC
|
|
80
78
|
db_url.password
|
81
79
|
)
|
82
80
|
if conn.status != PGconn::CONNECTION_OK
|
83
|
-
log(
|
81
|
+
log(:level => :error, :message => conn.error)
|
84
82
|
end
|
85
83
|
conn
|
86
84
|
end
|
@@ -90,7 +88,7 @@ module QC
|
|
90
88
|
end
|
91
89
|
|
92
90
|
def log(msg)
|
93
|
-
|
91
|
+
QC.log(msg)
|
94
92
|
end
|
95
93
|
|
96
94
|
end
|
@@ -3,9 +3,11 @@ module QC
|
|
3
3
|
extend self
|
4
4
|
|
5
5
|
def insert(q_name, method, args, chan=nil)
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
QC.log_yield(:action => "insert_job") do
|
7
|
+
s = "INSERT INTO #{TABLE_NAME} (q_name, method, args) VALUES ($1, $2, $3)"
|
8
|
+
res = Conn.execute(s, q_name, method, OkJson.encode(args))
|
9
|
+
Conn.notify(chan) if chan
|
10
|
+
end
|
9
11
|
end
|
10
12
|
|
11
13
|
def lock_head(q_name, top_bound)
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
module Scrolls
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def log(data, &blk)
|
7
|
+
Log.log(data, &blk)
|
8
|
+
end
|
9
|
+
|
10
|
+
def log_exception(data, e)
|
11
|
+
Log.log_exception(data, e)
|
12
|
+
end
|
13
|
+
|
14
|
+
module Log
|
15
|
+
extend self
|
16
|
+
|
17
|
+
LOG_LEVEL = (ENV["QC_LOG_LEVEL"] || 3).to_i
|
18
|
+
LOG_LEVEL_MAP = {
|
19
|
+
"fatal" => 0,
|
20
|
+
"error" => 1,
|
21
|
+
"warn" => 2,
|
22
|
+
"info" => 3,
|
23
|
+
"debug" => 4
|
24
|
+
}
|
25
|
+
|
26
|
+
attr_accessor :stream
|
27
|
+
|
28
|
+
def start(out = nil)
|
29
|
+
# This allows log_exceptions below to pick up the defined output,
|
30
|
+
# otherwise stream out to STDERR
|
31
|
+
@defined = out.nil? ? false : true
|
32
|
+
sync_stream(out)
|
33
|
+
end
|
34
|
+
|
35
|
+
def sync_stream(out = nil)
|
36
|
+
out = STDOUT if out.nil?
|
37
|
+
@stream = out
|
38
|
+
@stream.sync = true
|
39
|
+
end
|
40
|
+
|
41
|
+
def mtx
|
42
|
+
@mtx ||= Mutex.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def write(data)
|
46
|
+
if log_level_ok?(data[:level])
|
47
|
+
msg = unparse(data)
|
48
|
+
mtx.synchronize do
|
49
|
+
@stream.puts(msg)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def unparse(data)
|
55
|
+
data.map do |(k, v)|
|
56
|
+
if (v == true)
|
57
|
+
k.to_s
|
58
|
+
elsif v.is_a?(Float)
|
59
|
+
"#{k}=#{format("%.3f", v)}"
|
60
|
+
elsif v.nil?
|
61
|
+
nil
|
62
|
+
else
|
63
|
+
v_str = v.to_s
|
64
|
+
if (v_str =~ /^[a-zA-z0-9\-\_\.]+$/)
|
65
|
+
"#{k}=#{v_str}"
|
66
|
+
else
|
67
|
+
"#{k}=\"#{v_str.sub(/".*/, "...")}\""
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end.compact.join(" ")
|
71
|
+
end
|
72
|
+
|
73
|
+
def log(data, &blk)
|
74
|
+
unless blk
|
75
|
+
write(data)
|
76
|
+
else
|
77
|
+
start = Time.now
|
78
|
+
res = nil
|
79
|
+
log(data.merge(:at => :start))
|
80
|
+
begin
|
81
|
+
res = yield
|
82
|
+
rescue StandardError, Timeout::Error => e
|
83
|
+
log(data.merge(
|
84
|
+
:at => :exception,
|
85
|
+
:reraise => true,
|
86
|
+
:class => e.class,
|
87
|
+
:message => e.message,
|
88
|
+
:exception_id => e.object_id.abs,
|
89
|
+
:elapsed => Time.now - start
|
90
|
+
))
|
91
|
+
raise(e)
|
92
|
+
end
|
93
|
+
log(data.merge(:at => :finish, :elapsed => Time.now - start))
|
94
|
+
res
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def log_exception(data, e)
|
99
|
+
sync_stream(STDERR) unless @defined
|
100
|
+
log(data.merge(
|
101
|
+
:exception => true,
|
102
|
+
:class => e.class,
|
103
|
+
:message => e.message,
|
104
|
+
:exception_id => e.object_id.abs
|
105
|
+
))
|
106
|
+
if e.backtrace
|
107
|
+
bt = e.backtrace.reverse
|
108
|
+
bt[0, bt.size-6].each do |line|
|
109
|
+
log(data.merge(
|
110
|
+
:exception => true,
|
111
|
+
:exception_id => e.object_id.abs,
|
112
|
+
:site => line.gsub(/[`'"]/, "")
|
113
|
+
))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def log_level_ok?(level)
|
119
|
+
if level
|
120
|
+
LOG_LEVEL_MAP[level.to_s] <= LOG_LEVEL
|
121
|
+
else
|
122
|
+
true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
data/lib/queue_classic/worker.rb
CHANGED
@@ -1,26 +1,37 @@
|
|
1
1
|
module QC
|
2
2
|
class Worker
|
3
3
|
|
4
|
-
def initialize(
|
5
|
-
|
6
|
-
|
4
|
+
def initialize(*args)
|
5
|
+
if args.length == 5
|
6
|
+
q_name, top_bound, fork_worker, listening_worker, max_attempts = *args
|
7
|
+
elsif args.length <= 1
|
8
|
+
opts = args.first || {}
|
9
|
+
q_name = opts[:q_name] || QC::QUEUE
|
10
|
+
top_bound = opts[:top_bound] || QC::TOP_BOUND
|
11
|
+
fork_worker = opts[:fork_worker] || QC::FORK_WORKER
|
12
|
+
listening_worker = opts[:listening_worker] || QC::LISTENING_WORKER
|
13
|
+
max_attempts = opts[:max_attempts] || QC::MAX_LOCK_ATTEMPTS
|
14
|
+
else
|
15
|
+
raise ArgumentError, 'wrong number of arguments (expected no args, an options hash, or 5 separate args)'
|
16
|
+
end
|
7
17
|
|
18
|
+
@running = true
|
8
19
|
@queue = Queue.new(q_name, listening_worker)
|
9
|
-
log("worker queue=#{@queue.name}")
|
10
|
-
|
11
20
|
@top_bound = top_bound
|
12
|
-
log("worker top_bound=#{@top_bound}")
|
13
|
-
|
14
21
|
@fork_worker = fork_worker
|
15
|
-
log("worker fork=#{@fork_worker}")
|
16
|
-
|
17
22
|
@listening_worker = listening_worker
|
18
|
-
log("worker listen=#{@listening_worker}")
|
19
|
-
|
20
23
|
@max_attempts = max_attempts
|
21
|
-
log("max lock attempts =#{@max_attempts}")
|
22
|
-
|
23
24
|
handle_signals
|
25
|
+
|
26
|
+
log(
|
27
|
+
:level => :debug,
|
28
|
+
:action => "worker_initialized",
|
29
|
+
:queue => q_name,
|
30
|
+
:top_bound => top_bound,
|
31
|
+
:fork_worker => fork_worker,
|
32
|
+
:listening_worker => listening_worker,
|
33
|
+
:max_attempts => max_attempts
|
34
|
+
)
|
24
35
|
end
|
25
36
|
|
26
37
|
def running?
|
@@ -40,7 +51,7 @@ module QC
|
|
40
51
|
trap(sig) do
|
41
52
|
if running?
|
42
53
|
@running = false
|
43
|
-
log("
|
54
|
+
log(:level => :debug, :action => "handle_signal", :running => @running)
|
44
55
|
else
|
45
56
|
raise Interrupt
|
46
57
|
end
|
@@ -52,13 +63,10 @@ module QC
|
|
52
63
|
# your worker is forking and you need to
|
53
64
|
# re-establish database connectoins
|
54
65
|
def setup_child
|
55
|
-
log("forked worker running setup")
|
56
66
|
end
|
57
67
|
|
58
68
|
def start
|
59
|
-
log("worker starting")
|
60
69
|
while running?
|
61
|
-
log("worker running...")
|
62
70
|
if fork_worker?
|
63
71
|
fork_and_work
|
64
72
|
else
|
@@ -69,48 +77,44 @@ module QC
|
|
69
77
|
|
70
78
|
def fork_and_work
|
71
79
|
@cpid = fork { setup_child; work }
|
72
|
-
log(
|
80
|
+
log(:level => :debug, :action => :fork, :pid => @cpid)
|
73
81
|
Process.wait(@cpid)
|
74
82
|
end
|
75
83
|
|
76
84
|
def work
|
77
|
-
log("worker start working")
|
78
85
|
if job = lock_job
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
86
|
+
QC.log_yield(:level => :info, :action => "work_job", :job => job[:id]) do
|
87
|
+
begin
|
88
|
+
call(job)
|
89
|
+
rescue Object => e
|
90
|
+
log(:level => :debug, :action => "failed_work", :job => job[:id], :error => e.inspect)
|
91
|
+
handle_failure(job, e)
|
92
|
+
ensure
|
93
|
+
@queue.delete(job[:id])
|
94
|
+
log(:level => :debug, :action => "delete_job", :job => job[:id])
|
83
95
|
end
|
84
|
-
rescue Object => e
|
85
|
-
log("worker failed job=#{job[:id]} exception=#{e.inspect}")
|
86
|
-
handle_failure(job, e)
|
87
|
-
ensure
|
88
|
-
@queue.delete(job[:id])
|
89
|
-
log("worker deleted job=#{job[:id]}")
|
90
96
|
end
|
91
97
|
end
|
92
98
|
end
|
93
99
|
|
94
100
|
def lock_job
|
95
|
-
log(
|
101
|
+
log(:level => :debug, :action => "lock_job")
|
96
102
|
attempts = 0
|
97
103
|
job = nil
|
98
104
|
until job
|
99
105
|
job = @queue.lock(@top_bound)
|
100
106
|
if job.nil?
|
101
|
-
log("
|
107
|
+
log(:level => :debug, :action => "failed_lock", :attempts => attempts)
|
102
108
|
if attempts < @max_attempts
|
103
109
|
seconds = 2**attempts
|
104
110
|
wait(seconds)
|
105
|
-
log("worker tries again")
|
106
111
|
attempts += 1
|
107
112
|
next
|
108
113
|
else
|
109
|
-
log("worker reached max attempts. max=#{@max_attempts}")
|
110
114
|
break
|
111
115
|
end
|
112
116
|
else
|
113
|
-
log("
|
117
|
+
log(:level => :debug, :action => "finished_lock", :job => job[:id])
|
114
118
|
end
|
115
119
|
end
|
116
120
|
job
|
@@ -125,14 +129,14 @@ module QC
|
|
125
129
|
|
126
130
|
def wait(t)
|
127
131
|
if can_listen?
|
128
|
-
log("
|
132
|
+
log(:level => :debug, :action => "listen_wait", :wait => t)
|
129
133
|
Conn.listen(@queue.chan)
|
130
134
|
Conn.wait_for_notify(t)
|
131
135
|
Conn.unlisten(@queue.chan)
|
132
136
|
Conn.drain_notify
|
133
|
-
log(
|
137
|
+
log(:level => :debug, :action => "finished_listening")
|
134
138
|
else
|
135
|
-
log("
|
139
|
+
log(:level => :debug, :action => "sleep_wait", :wait => t)
|
136
140
|
Kernel.sleep(t)
|
137
141
|
end
|
138
142
|
end
|
@@ -146,8 +150,8 @@ module QC
|
|
146
150
|
puts "!"
|
147
151
|
end
|
148
152
|
|
149
|
-
def log(
|
150
|
-
|
153
|
+
def log(data)
|
154
|
+
QC.log(data)
|
151
155
|
end
|
152
156
|
|
153
157
|
end
|
data/lib/queue_classic.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
require "pg"
|
2
|
-
|
3
|
-
require "logger"
|
4
2
|
require "uri"
|
5
3
|
|
6
4
|
$: << File.expand_path(__FILE__, "lib")
|
7
5
|
|
6
|
+
require "queue_classic/scrolls"
|
8
7
|
require "queue_classic/okjson"
|
9
8
|
require "queue_classic/conn"
|
10
9
|
require "queue_classic/queries"
|
@@ -12,6 +11,8 @@ require "queue_classic/queue"
|
|
12
11
|
require "queue_classic/worker"
|
13
12
|
|
14
13
|
module QC
|
14
|
+
# ENV["QC_LOG_LEVEL"] is used in Scrolls
|
15
|
+
Scrolls::Log.start
|
15
16
|
|
16
17
|
Root = File.expand_path("..", File.dirname(__FILE__))
|
17
18
|
SqlFunctions = File.join(QC::Root, "/sql/ddl.sql")
|
@@ -22,9 +23,6 @@ module QC
|
|
22
23
|
ENV["DATABASE_URL"] ||
|
23
24
|
raise(ArgumentError, "missing QC_DATABASE_URL or DATABASE_URL")
|
24
25
|
|
25
|
-
# export QC_LOG_LEVEL=`ruby -r "logger" -e "puts Logger::ERROR"`
|
26
|
-
LOG_LEVEL = (ENV["QC_LOG_LEVEL"] || Logger::DEBUG).to_i
|
27
|
-
|
28
26
|
# You can use the APP_NAME to query for
|
29
27
|
# postgres related process information in the
|
30
28
|
# pg_stat_activity table. Don't set this unless
|
@@ -66,12 +64,6 @@ module QC
|
|
66
64
|
# as the max exponent.
|
67
65
|
MAX_LOCK_ATTEMPTS = (ENV["QC_MAX_LOCK_ATTEMPTS"] || 5).to_i
|
68
66
|
|
69
|
-
|
70
|
-
# Setup the logger
|
71
|
-
Log = Logger.new($stdout)
|
72
|
-
Log.level = LOG_LEVEL
|
73
|
-
Log.info("program=queue_classic log=true")
|
74
|
-
|
75
67
|
# Defer method calls on the QC module to the
|
76
68
|
# default queue. This facilitates QC.enqueue()
|
77
69
|
def self.method_missing(sym, *args, &block)
|
@@ -84,4 +76,21 @@ module QC
|
|
84
76
|
end
|
85
77
|
end
|
86
78
|
|
79
|
+
def self.log_yield(data)
|
80
|
+
begin
|
81
|
+
t0 = Time.now
|
82
|
+
yield
|
83
|
+
rescue => e
|
84
|
+
log({:level => :error, :error => e.class, :message => e.message.strip}.merge(data))
|
85
|
+
raise
|
86
|
+
ensure
|
87
|
+
t = Integer((Time.now - t0)*1000)
|
88
|
+
log(data.merge(:elapsed => t)) unless e
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.log(data)
|
93
|
+
Scrolls.log({:lib => :queue_classic}.merge(data))
|
94
|
+
end
|
95
|
+
|
87
96
|
end
|
data/readme.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# queue_classic
|
2
2
|
|
3
|
-
v2.0.
|
3
|
+
v2.0.0rc9
|
4
4
|
|
5
5
|
queue_classic is a PostgreSQL-backed queueing library that is focused on
|
6
6
|
concurrent job locking, minimizing database load & providing a simple &
|
@@ -14,6 +14,7 @@ queue_classic features:
|
|
14
14
|
* Forking workers
|
15
15
|
* Postgres' rock-solid locking mechanism
|
16
16
|
* Fuzzy-FIFO support [academic paper](http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/Lock_Free.pdf)
|
17
|
+
* Instrumentation via log output
|
17
18
|
* Long term support
|
18
19
|
|
19
20
|
## Proven
|
@@ -387,6 +388,17 @@ a method that you can override. This method will be passed 2 arguments: the
|
|
387
388
|
exception instance and the job. Here are a few examples of things you might want
|
388
389
|
to do inside `handle_failure()`.
|
389
390
|
|
391
|
+
## Instrumentation
|
392
|
+
|
393
|
+
QC will log elapsed time, errors and general usage in the form of data.
|
394
|
+
To customize the output of the log data, override `QC.log` and `QC.log_yield`.
|
395
|
+
By default, QC uses a simple wrapper around $stdout to put the log data in k=v
|
396
|
+
format. For instance:
|
397
|
+
|
398
|
+
```
|
399
|
+
lib=queue_classic level=info action=insert_job elapsed=16
|
400
|
+
```
|
401
|
+
|
390
402
|
## Tips and Tricks
|
391
403
|
|
392
404
|
### Running Synchronously for tests
|
data/test/helper.rb
CHANGED
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: 2.0.
|
4
|
+
version: 2.0.0rc10
|
5
5
|
prerelease: 5
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-02-29 00:00:00.000000000Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: pg
|
16
|
-
requirement: &
|
16
|
+
requirement: &13897100 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: 0.13.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *13897100
|
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.
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- lib/queue_classic/conn.rb
|
37
37
|
- lib/queue_classic/okjson.rb
|
38
38
|
- lib/queue_classic/worker.rb
|
39
|
+
- lib/queue_classic/scrolls.rb
|
39
40
|
- lib/queue_classic/queue.rb
|
40
41
|
- lib/queue_classic/tasks.rb
|
41
42
|
- lib/queue_classic/queries.rb
|