queue_classic 2.0.0rc9 → 2.0.0rc10

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@ module QC
3
3
  extend self
4
4
 
5
5
  def execute(stmt, *params)
6
- log("executing #{stmt.inspect}, #{params.inspect}")
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("execute exception=#{e.inspect}")
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("draining notifications")
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("received notification #{event}")
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("establishing connection")
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("connection error=#{conn.error}")
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
- Log.info(msg)
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
- 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
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
@@ -1,26 +1,37 @@
1
1
  module QC
2
2
  class Worker
3
3
 
4
- def initialize(q_name, top_bound, fork_worker, listening_worker, max_attempts)
5
- log("worker initialized")
6
- @running = true
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("worker running=#{@running}")
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("worker forked pid=#{@cpid}")
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
- log("worker locked job=#{job[:id]}")
80
- begin
81
- call(job).tap do
82
- log("worker finished job=#{job[:id]}")
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("worker attempting a lock")
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("worker missed lock attempt=#{attempts}")
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("worker successfully locked job")
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("worker waiting on LISTEN")
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("worker finished LISTEN")
137
+ log(:level => :debug, :action => "finished_listening")
134
138
  else
135
- log("worker sleeps seconds=#{t}")
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(msg)
150
- Log.info(msg)
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.0rc4
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
@@ -7,8 +7,6 @@ require "queue_classic"
7
7
  require "minitest/unit"
8
8
  MiniTest::Unit.autorun
9
9
 
10
- QC::Log.level = Logger::ERROR
11
-
12
10
  class QCTest < MiniTest::Unit::TestCase
13
11
 
14
12
  def setup
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.0rc9
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: &20818900 !ruby/object:Gem::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: *20818900
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