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.
@@ -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