timberline 0.6.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +8 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/CHANGELOG +3 -0
- data/README.markdown +22 -16
- data/lib/timberline/anonymous_worker.rb +48 -0
- data/lib/timberline/config.rb +32 -2
- data/lib/timberline/envelope.rb +30 -3
- data/lib/timberline/exceptions.rb +17 -0
- data/lib/timberline/queue.rb +115 -0
- data/lib/timberline/version.rb +2 -1
- data/lib/timberline/worker.rb +74 -0
- data/lib/timberline.rb +61 -47
- data/spec/lib/timberline_spec.rb +0 -41
- data/timberline.gemspec +1 -0
- metadata +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e8abd9f7a068aed49df25b77c38489047e89ac1
|
4
|
+
data.tar.gz: ab2306c2f9ae72ee849e2fef5a4aa632b284b67a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5947f1e61eb13ccdf5663f1a678abc175683dbefcfd0e500d4844d14d287cd947a55120c807f5848a43ee7a9672457474ce123e64ed0923e5bf130f4185b0115
|
7
|
+
data.tar.gz: 4394761078279b91e7ccf86a2bd5c1b29eeb5755a05458b51d03339a4b6944d699071bc039c653f54fe6cf8f1a4beced5abfb89d8b1a1e5d9efc925531df649f
|
data/.yardoc/checksums
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
lib/timberline.rb bde38a760f2c89941eb1db0dd9bfe16824eacb90
|
2
|
+
lib/timberline/queue.rb f4463942e074f53cc10b14adaa9d00ce6a9fe606
|
3
|
+
lib/timberline/config.rb b53bd318344a956a8efa1058319c88411c982874
|
4
|
+
lib/timberline/worker.rb 3245a5b986391d28c1a0ea745fa09df5df375cf7
|
5
|
+
lib/timberline/version.rb 926b9f154a084b54b1d8636c711cbaa7d8166998
|
6
|
+
lib/timberline/envelope.rb fd34ad408e519b1655bf8163b4d81fb0600f7ca4
|
7
|
+
lib/timberline/exceptions.rb 3dcda3d2e61e920696eba97d2b84e1a1df642a7a
|
8
|
+
lib/timberline/anonymous_worker.rb 2f8fb80e35f3702ce61384d2b18ba334387046ef
|
Binary file
|
Binary file
|
data/.yardoc/proxy_types
ADDED
Binary file
|
data/CHANGELOG
CHANGED
data/README.markdown
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Timberline
|
2
2
|
|
3
|
-

|
3
|
+

|
4
|
+
[](http://badge.fury.io/rb/timberline)
|
4
5
|
|
5
6
|
## Purpose
|
6
7
|
|
@@ -21,8 +22,28 @@ kind of issues you might be dealing with:
|
|
21
22
|
jobs as fast as you possibly can. To that end, Timberline uses blocking reads
|
22
23
|
in Redis to pull jobs off of the queue as soon as they're available.
|
23
24
|
|
25
|
+
## Documentation
|
26
|
+
|
27
|
+
Documentation for Timberline is available on rubydoc.info [here](http://rubydoc.info/github/treehouse/timberline/frames). The code itself is documented with YARD.
|
28
|
+
|
24
29
|
## Concepts
|
25
30
|
|
31
|
+
### The Envelope
|
32
|
+
|
33
|
+
Sounds SOAPy, I know. The envelope is a simple object that wraps the data you
|
34
|
+
want to put on the queue - it's responsible for tracking things like the job ID,
|
35
|
+
the queue it was put on, how many times it's been retried, etc., etc. It's also
|
36
|
+
accessible to both the queue processor and whatever is putting jobs on the
|
37
|
+
queue, so if you want to be able to check in on the administrative details (or
|
38
|
+
add some of your own) this is a great place to do it instead of muddying up the
|
39
|
+
meat of your message.
|
40
|
+
|
41
|
+
### Workers
|
42
|
+
|
43
|
+
Processing items on a Timberline queue is handled by Workers. A Worker can be
|
44
|
+
created as simply as providing a block that executes on job items, or you can
|
45
|
+
write your own Workers that perform special functionality.
|
46
|
+
|
26
47
|
### Retries
|
27
48
|
|
28
49
|
Sometimes jobs just fail because of something that was outside of your control.
|
@@ -41,16 +62,6 @@ explicitly marked as bad jobs, or when they've been retried the maximum number
|
|
41
62
|
of times. You can then check the jobs out and resubmit them to their original
|
42
63
|
queue after you fix the issue.
|
43
64
|
|
44
|
-
### The Envelope
|
45
|
-
|
46
|
-
Sounds SOAPy, I know. The envelope is a simple object that wraps the data you
|
47
|
-
want to put on the queue - it's responsible for tracking things like the job ID,
|
48
|
-
the queue it was put on, how many times it's been retried, etc., etc. It's also
|
49
|
-
accessible to both the queue processor and whatever is putting jobs on the
|
50
|
-
queue, so if you want to be able to check in on the administrative details (or
|
51
|
-
add some of your own) this is a great place to do it instead of muddying up the
|
52
|
-
meat of your message.
|
53
|
-
|
54
65
|
## Usage
|
55
66
|
|
56
67
|
Timberline is designed to be as easy to work with as possible, and operates almost
|
@@ -170,11 +181,6 @@ Still to be done:
|
|
170
181
|
|
171
182
|
- **Monitor** - A simple Sinatra interface for monitoring the statuses of queues and
|
172
183
|
observing/resubmitting errored-out jobs.
|
173
|
-
- **Documentation** - need to get Tomdoc added so that the API is more completely
|
174
|
-
documented. For the time being, though, there are some fairly comprehensive
|
175
|
-
test suites.
|
176
|
-
- **Refactor** - the singleton model made sense at some point for Timberline but now it's
|
177
|
-
cumbersome. Need to rewrite some of the basic stuff to be more OO-appropriate.
|
178
184
|
- **Forking** - Timberline should support forking in its watcher model so that jobs can
|
179
185
|
be run in parallel and independently of each other.
|
180
186
|
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Timberline
|
2
|
+
# The AnonymousWorker is exactly what it says on the tin - a way to process a queue
|
3
|
+
# without defining a new class, by instead just passing in a block that will be
|
4
|
+
# executed for each item on the queue as it's popped.
|
5
|
+
#
|
6
|
+
class AnonymousWorker < Worker
|
7
|
+
|
8
|
+
# Creates a new AnonymousWorker.
|
9
|
+
# The block's binding will be updated to give it access to retry_item
|
10
|
+
# and error_item so that the block can easily control the processing
|
11
|
+
# flow for queued items.
|
12
|
+
#
|
13
|
+
# @param [String] queue_name the name of the queue to watch
|
14
|
+
# @param [Block] block the block to run against each item that gets popped
|
15
|
+
# off the queue.
|
16
|
+
#
|
17
|
+
# @example Creating a simple AnonymousWorker
|
18
|
+
# AnonymousWorker.new "test_queue" { |item| puts item.contents }
|
19
|
+
#
|
20
|
+
# @return [AnonymousWorker]
|
21
|
+
#
|
22
|
+
def initialize(queue_name, &block)
|
23
|
+
super(queue_name)
|
24
|
+
@block = block
|
25
|
+
fix_block_binding
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see Timberline::Worker#watch
|
29
|
+
#
|
30
|
+
def process_item(item)
|
31
|
+
@block.call(item, self)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def fix_block_binding
|
36
|
+
binding = @block.binding
|
37
|
+
binding.eval <<-HERE
|
38
|
+
def retry_item(item)
|
39
|
+
Worker.retry_item(item)
|
40
|
+
end
|
41
|
+
|
42
|
+
def error_item(item)
|
43
|
+
Worker.error_item(item)
|
44
|
+
end
|
45
|
+
HERE
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/timberline/config.rb
CHANGED
@@ -1,8 +1,34 @@
|
|
1
1
|
class Timberline
|
2
|
+
# Object that manages Timberline configuration. Responsible for Redis configs
|
3
|
+
# as well as Timberline-specific configuration values, like how many times an
|
4
|
+
# item should be retried in a queue.
|
5
|
+
#
|
6
|
+
# @attr [Integer] database part of the redis configuration - index of the
|
7
|
+
# redis database to use
|
8
|
+
# @attr [String] host part of the redis configuration - the hostname of the
|
9
|
+
# redis server
|
10
|
+
# @attr [Integer] port part of the redis configuration - the port of the
|
11
|
+
# redis server
|
12
|
+
# @attr [Integer] timeout part of the redis configuration - the timeout for
|
13
|
+
# the redis server
|
14
|
+
# @attr [String] password part of the redis configuration - the password for
|
15
|
+
# the redis server
|
16
|
+
# @attr [Logger] logger part of the redis configuration - the logger to use
|
17
|
+
# for the redis connection
|
18
|
+
# @attr [String] namespace the redis namespace for the keys that timberline
|
19
|
+
# will create and manage
|
20
|
+
# @attr [Integer] max_retries the number of times that an item on the queue
|
21
|
+
# should be allowed to retry itself before it is placed on the error queue
|
22
|
+
# @attr [Integer] stat_timeout the number of minutes that stats will stay live
|
23
|
+
# in redis before they are expired
|
24
|
+
#
|
2
25
|
class Config
|
3
|
-
attr_accessor :database, :host, :port, :timeout, :password,
|
26
|
+
attr_accessor :database, :host, :port, :timeout, :password,
|
27
|
+
:logger, :namespace, :max_retries, :stat_timeout
|
4
28
|
|
5
|
-
|
29
|
+
# Attemps to load configuration from TIMBERLINE_YAML, if it exists.
|
30
|
+
# Otherwise creates a default Config object.
|
31
|
+
def initialize
|
6
32
|
if defined? TIMBERLINE_YAML
|
7
33
|
if File.exists?(TIMBERLINE_YAML)
|
8
34
|
yaml = YAML.load_file(TIMBERLINE_YAML)
|
@@ -13,18 +39,22 @@ class Timberline
|
|
13
39
|
end
|
14
40
|
end
|
15
41
|
|
42
|
+
# @return [String] the configured redis namespace, with a default of 'timberline'
|
16
43
|
def namespace
|
17
44
|
@namespace ||= 'timberline'
|
18
45
|
end
|
19
46
|
|
47
|
+
# @return [Integer] the configured maximum number of retries, with a default of 5
|
20
48
|
def max_retries
|
21
49
|
@max_retries ||= 5
|
22
50
|
end
|
23
51
|
|
52
|
+
# @return [Integer] the configured lifetime of stats (in minutes), with a default of 60
|
24
53
|
def stat_timeout
|
25
54
|
@stat_timeout ||= 60
|
26
55
|
end
|
27
56
|
|
57
|
+
# @return [Hash] a Redis-ready hash for use in instantiating a new redis object.
|
28
58
|
def redis_config
|
29
59
|
config = {}
|
30
60
|
|
data/lib/timberline/envelope.rb
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
class Timberline
|
2
|
+
# An Envelope in Timberline is what gets passed along on the queue.
|
3
|
+
# The message itself - the part that the workers should intend to operate on -
|
4
|
+
# is stored in the `contents` field of the Envelope. Any other data on the
|
5
|
+
# envelope is considered metadata. Metadata is mostly used by Timberline itself,
|
6
|
+
# but is also exposed to the end user in case they have any need for it.
|
7
|
+
#
|
8
|
+
# @attr [#to_json] contents the contents of the envelope; the message to be
|
9
|
+
# passed on the queue
|
10
|
+
# @attr [Hash] metadata the metadata information associated with the envelope
|
11
|
+
#
|
2
12
|
class Envelope
|
3
13
|
|
14
|
+
# Given a JSON string representing an envelope, build an Envelope object
|
15
|
+
# with the appropriate data.
|
16
|
+
#
|
17
|
+
# @param [String] json_string the JSON string to parse
|
18
|
+
# @return [Envelope]
|
19
|
+
#
|
4
20
|
def self.from_json(json_string)
|
5
21
|
envelope = Envelope.new
|
6
22
|
envelope.instance_variable_set("@metadata", JSON.parse(json_string))
|
@@ -11,16 +27,30 @@ class Timberline
|
|
11
27
|
attr_accessor :contents
|
12
28
|
attr_reader :metadata
|
13
29
|
|
30
|
+
# Instantiates an Envelope with no metadata and nil contents.
|
31
|
+
# @return [Envelope]
|
32
|
+
#
|
14
33
|
def initialize
|
15
34
|
@metadata = {}
|
16
35
|
end
|
17
36
|
|
37
|
+
# Builds a JSON string version of the envelope.
|
38
|
+
#
|
39
|
+
# @raise [MissingContentException] if the envelope is empty (has no contents)
|
40
|
+
# @return [String] a JSON representation of the envelope
|
41
|
+
#
|
18
42
|
def to_s
|
19
43
|
raise MissingContentException if contents.nil? || contents.empty?
|
20
44
|
|
21
45
|
JSON.unparse(build_envelope_hash)
|
22
46
|
end
|
23
47
|
|
48
|
+
# Passes any missing methods on to the metadata hash to provide better access.
|
49
|
+
# @example Easily read from metadata
|
50
|
+
# some_envelope.origin_queue # returns metadata["origin_queue"]
|
51
|
+
# @example Easily write to metadata
|
52
|
+
# some_envelope.origin_queue = "test_queue" # sets metadata["origin_queue"] to "test_queue"
|
53
|
+
#
|
24
54
|
def method_missing(method_name, *args)
|
25
55
|
method_name = method_name.to_s
|
26
56
|
if method_name[-1] == "="
|
@@ -41,7 +71,4 @@ class Timberline
|
|
41
71
|
end
|
42
72
|
|
43
73
|
end
|
44
|
-
|
45
|
-
class MissingContentException < Exception
|
46
|
-
end
|
47
74
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Timberline
|
2
|
+
|
3
|
+
# Used to indicate that an Envelope does not yet have contents, but is being
|
4
|
+
# operated on as though it should.
|
5
|
+
#
|
6
|
+
class MissingContentException < Exception; end
|
7
|
+
|
8
|
+
# Raised to indicate that the item currently being processed was retried.
|
9
|
+
# Prevents Workers from treating the item as a success.
|
10
|
+
#
|
11
|
+
class ItemRetried < Exception; end
|
12
|
+
|
13
|
+
# Raised to indicate that the item currently being processed experienced a
|
14
|
+
# fatal error. Prevents Workers from treating the item as a success.
|
15
|
+
#
|
16
|
+
class ItemErrored < Exception; end
|
17
|
+
end
|
data/lib/timberline/queue.rb
CHANGED
@@ -1,7 +1,29 @@
|
|
1
1
|
class Timberline
|
2
|
+
# Queue is the heart and soul of Timberline, which makes sense
|
3
|
+
# considering that it's a queueing library. This object represents
|
4
|
+
# a queue in redis (really just a list of strings) and is responsible
|
5
|
+
# for reading to the queue, writing from the queue, maintaining queue
|
6
|
+
# statistics, and managing other queue actions (like pausing and deleting).
|
7
|
+
#
|
8
|
+
# @attr_reader [String] queue_name the name of this queue
|
9
|
+
# @attr_reader [Integer] read_timeout how long this queue should wait, in
|
10
|
+
# seconds, before determining that there isn't anything to read off of the
|
11
|
+
# queue.
|
12
|
+
#
|
2
13
|
class Queue
|
3
14
|
attr_reader :queue_name, :read_timeout
|
4
15
|
|
16
|
+
# Build a new Queue object.
|
17
|
+
#
|
18
|
+
# @param [String] queue_name the redis queue that this object should represent
|
19
|
+
# @param [Hash] opts the options for creating this queue
|
20
|
+
# @option opts [Integer] :read_timeout the read_timeout for this queue.
|
21
|
+
# defaults to 0 (which effectively disables the timeout).
|
22
|
+
# @option opts [boolean] :hidden whether this queue should be hidden from
|
23
|
+
# Timberline's #all_queues list. Defaults to false.
|
24
|
+
# @raise [ArgumentError] if queue_name is not provided
|
25
|
+
# @return [Queue]
|
26
|
+
#
|
5
27
|
def initialize(queue_name, opts = {})
|
6
28
|
read_timeout = opts.fetch(:read_timeout, 0)
|
7
29
|
hidden = opts.fetch(:hidden, false)
|
@@ -16,6 +38,11 @@ class Timberline
|
|
16
38
|
end
|
17
39
|
end
|
18
40
|
|
41
|
+
# Delete this queue, removing it from redis and all other references to it
|
42
|
+
# from Timberline.
|
43
|
+
#
|
44
|
+
# @return as Redis#srem
|
45
|
+
#
|
19
46
|
def delete
|
20
47
|
@redis.del @queue_name
|
21
48
|
@redis.keys("#{@queue_name}:*").each do |key|
|
@@ -24,10 +51,21 @@ class Timberline
|
|
24
51
|
@redis.srem "timberline_queue_names", @queue_name
|
25
52
|
end
|
26
53
|
|
54
|
+
# The current number of items on the queue waiting to be processed.
|
55
|
+
#
|
56
|
+
# @return [Integer]
|
57
|
+
#
|
27
58
|
def length
|
28
59
|
@redis.llen @queue_name
|
29
60
|
end
|
30
61
|
|
62
|
+
# Uses a blocking read from redis to pull the next item off the queue. If
|
63
|
+
# the queue is paused, this method will block until the queue is unpaused,
|
64
|
+
# at which point it will move on to the blocking read.
|
65
|
+
#
|
66
|
+
# @return [Timberline::Envelope] the Envelope representation of the item
|
67
|
+
# that was pulled off the queue, or nil if the read timed out.
|
68
|
+
#
|
31
69
|
def pop
|
32
70
|
block_while_paused
|
33
71
|
|
@@ -40,6 +78,14 @@ class Timberline
|
|
40
78
|
end
|
41
79
|
end
|
42
80
|
|
81
|
+
# Pushes the specified data onto the queue.
|
82
|
+
#
|
83
|
+
# @param [#to_json, Timberline::Envelope] contents either contents that can
|
84
|
+
# be converted to JSON and stuffed in an Envelope, or an Envelope itself
|
85
|
+
# that needs to be put on the queue.
|
86
|
+
# @param [Hash] metadata metadata that will be attached to the envelope for
|
87
|
+
# contents.
|
88
|
+
#
|
43
89
|
def push(contents, metadata = {})
|
44
90
|
case contents
|
45
91
|
when Envelope
|
@@ -49,34 +95,70 @@ class Timberline
|
|
49
95
|
end
|
50
96
|
end
|
51
97
|
|
98
|
+
# Puts this queue into paused mode.
|
99
|
+
# @see Timberline::Queue#pop
|
100
|
+
#
|
52
101
|
def pause
|
53
102
|
@redis[attr("paused")] = "true"
|
54
103
|
end
|
55
104
|
|
105
|
+
# Takes this queue back out of paused mode.
|
106
|
+
# @see Timberline::Queue#pop
|
107
|
+
#
|
56
108
|
def unpause
|
57
109
|
@redis[attr("paused")] = "false"
|
58
110
|
end
|
59
111
|
|
112
|
+
# Indicates whether or not this queue is currently in paused mode.
|
113
|
+
# @return [boolean]
|
114
|
+
#
|
60
115
|
def paused?
|
61
116
|
@redis[attr("paused")] == "true"
|
62
117
|
end
|
63
118
|
|
119
|
+
# Given a key, create a string namespaced to this queue name.
|
120
|
+
# This method is used to keep redis keys tidy.
|
121
|
+
#
|
122
|
+
# @return [String]
|
123
|
+
#
|
64
124
|
def attr(key)
|
65
125
|
"#{@queue_name}:#{key}"
|
66
126
|
end
|
67
127
|
|
128
|
+
# The number of items that have encountered fatal errors on the queue
|
129
|
+
# during the last [stat_timeout] minutes.
|
130
|
+
#
|
131
|
+
# @return [Integer]
|
132
|
+
#
|
68
133
|
def number_errors
|
69
134
|
Timberline.redis.xcard attr("error_stats")
|
70
135
|
end
|
71
136
|
|
137
|
+
# The number of items that have been retried on the queue
|
138
|
+
# during the last [stat_timeout] minutes.
|
139
|
+
#
|
140
|
+
# @return [Integer]
|
141
|
+
#
|
72
142
|
def number_retries
|
73
143
|
Timberline.redis.xcard attr("retry_stats")
|
74
144
|
end
|
75
145
|
|
146
|
+
# The number of items that were processed successfully for this queue
|
147
|
+
# during the last [stat_timeout] minutes.
|
148
|
+
#
|
149
|
+
# @return [Integer]
|
150
|
+
#
|
76
151
|
def number_successes
|
77
152
|
Timberline.redis.xcard attr("success_stats")
|
78
153
|
end
|
79
154
|
|
155
|
+
# Given all of the successful jobs that were executed in the last
|
156
|
+
# [stat_timeout] minutes, determine how long on average those jobs
|
157
|
+
# took to execute.
|
158
|
+
#
|
159
|
+
# @return [Float] the average execution time for successful jobs in the last
|
160
|
+
# [stat_timeout] minutes.
|
161
|
+
#
|
80
162
|
def average_execution_time
|
81
163
|
successes = Timberline.redis.xmembers(attr("success_stats")).map { |item| Envelope.from_json(item)}
|
82
164
|
times = successes.map do |item|
|
@@ -96,6 +178,15 @@ class Timberline
|
|
96
178
|
end
|
97
179
|
end
|
98
180
|
|
181
|
+
# Given an item that needs to be retried, increment the retry count,
|
182
|
+
# add any appropriate metadata about the retry, and push it back onto
|
183
|
+
# the queue. If the item has already been retried the maximum number of
|
184
|
+
# times, pass it on to error_item instead.
|
185
|
+
#
|
186
|
+
# @see Timberline::Queue#error_item
|
187
|
+
#
|
188
|
+
# @param [Envelope] item an item that needs to be retried
|
189
|
+
#
|
99
190
|
def retry_item(item)
|
100
191
|
if (item.retries < Timberline.max_retries)
|
101
192
|
item.retries += 1
|
@@ -107,26 +198,50 @@ class Timberline
|
|
107
198
|
end
|
108
199
|
end
|
109
200
|
|
201
|
+
# Given an item that errored out in processing, add any appropriate metadata
|
202
|
+
# about the error, track it as a statistic, and push it onto the error queue.
|
203
|
+
#
|
204
|
+
# @param [Envelope] item an item that has fatally errored
|
205
|
+
#
|
110
206
|
def error_item(item)
|
111
207
|
item.fatal_error_at = Time.now.to_f
|
112
208
|
add_error_stat(item)
|
113
209
|
self.error_queue.push(item)
|
114
210
|
end
|
115
211
|
|
212
|
+
# Stores an item from the queue that was retried so we can keep track
|
213
|
+
# of things like how many retries have been attempted on this queue, etc.
|
214
|
+
#
|
215
|
+
# @param [Envelope] item an item that fatally errored on this queue
|
216
|
+
#
|
116
217
|
def add_retry_stat(item)
|
117
218
|
add_stat_for_key(attr("retry_stats"), item)
|
118
219
|
end
|
119
220
|
|
221
|
+
# Stores an item from the queue that fatally errored so we can keep track
|
222
|
+
# of things like how many errors have occurred on this queue, etc.
|
223
|
+
#
|
224
|
+
# @param [Envelope] item an item that fatally errored on this queue
|
225
|
+
#
|
120
226
|
def add_error_stat(item)
|
121
227
|
add_stat_for_key(attr("error_stats"), item)
|
122
228
|
end
|
123
229
|
|
230
|
+
# Stores a successfully processed queue item as a statistic so we can keep
|
231
|
+
# track of things like average execution time, number of successes, etc.
|
232
|
+
#
|
233
|
+
# @param [Envelope] item an item that was processed successfully for this
|
234
|
+
# queue
|
235
|
+
#
|
124
236
|
def add_success_stat(item)
|
125
237
|
add_stat_for_key(attr("success_stats"), item)
|
126
238
|
rescue Exception => e
|
127
239
|
$stderr.puts "Success Stat Error: #{e.inspect}, Item: #{item.inspect}"
|
128
240
|
end
|
129
241
|
|
242
|
+
# @return [Timberline::Queue] a (hidden) Queue object where this queue's
|
243
|
+
# errors are pushed.
|
244
|
+
#
|
130
245
|
def error_queue
|
131
246
|
@error_queue ||= Timberline.queue(attr("errors"), hidden: true)
|
132
247
|
end
|
data/lib/timberline/version.rb
CHANGED
@@ -0,0 +1,74 @@
|
|
1
|
+
class Timberline
|
2
|
+
# Worker is the base class for Timberline Workers. It defines the basics for
|
3
|
+
# processing items off of a queue; the idea is that creating your own worker
|
4
|
+
# is as easy as extending Worker and implementing #process_item. You can also
|
5
|
+
# override #keep_watching? and #initialize to provide your own custom behavior
|
6
|
+
# easily, although this is not necessary.
|
7
|
+
#
|
8
|
+
class Worker
|
9
|
+
# Creates a new worker that will watch a specific queue.
|
10
|
+
# @param [String] queue_name the name of the queue to watch.
|
11
|
+
#
|
12
|
+
def initialize(queue_name)
|
13
|
+
@queue = Queue.new(queue_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Run the watch loop for this worker. As long as #keep_watching?
|
17
|
+
# returns true, this will pop items off the queue and process them
|
18
|
+
# with #process_item. This method is also responsible for managing
|
19
|
+
# some extra timberline metadata (tracking when processing starts and
|
20
|
+
# stops, for example) and shouldn't typically be overridden when you
|
21
|
+
# define your own worker.
|
22
|
+
#
|
23
|
+
def watch
|
24
|
+
while(keep_watching?)
|
25
|
+
item = @queue.pop
|
26
|
+
item.started_processing_at = Time.now.to_f
|
27
|
+
|
28
|
+
begin
|
29
|
+
process_item(item)
|
30
|
+
rescue ItemRetried, ItemErrored
|
31
|
+
next
|
32
|
+
end
|
33
|
+
|
34
|
+
item.finished_processing_at = Time.now.to_f
|
35
|
+
@queue.add_success_stat(item)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Given an item off of the queue, process it appropriately.
|
40
|
+
# Not implemented in Worker, as Worker is just a base class.
|
41
|
+
#
|
42
|
+
def process_item(item)
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
# Determine whether or not the worker loop in #watch should continue
|
47
|
+
# executing. By default this is always true.
|
48
|
+
#
|
49
|
+
# @return [boolean]
|
50
|
+
def keep_watching?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Given an item this worker is processing, have the queue mark it
|
55
|
+
# as fatally errored and raise an ItemErrored exception so that the
|
56
|
+
# watch loop can process it correctly
|
57
|
+
#
|
58
|
+
# @raise [Timberline::ItemErrored]
|
59
|
+
def error_item(item)
|
60
|
+
@queue.error_item(item)
|
61
|
+
raise Timberline::ItemErrored
|
62
|
+
end
|
63
|
+
|
64
|
+
# Given an item this worker is processing, have the queue mark it
|
65
|
+
# attempt to retry it and raise an ItemRetried exception so that the
|
66
|
+
# watch loop can process it correctly
|
67
|
+
#
|
68
|
+
# @raise [Timberline::ItemRetried]
|
69
|
+
def retry_item(item)
|
70
|
+
@queue.retry_item(item)
|
71
|
+
raise Timberline::ItemRetried
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/timberline.rb
CHANGED
@@ -7,16 +7,35 @@ require 'redis-namespace'
|
|
7
7
|
require 'redis-expiring-set/monkeypatch'
|
8
8
|
|
9
9
|
require_relative "timberline/version"
|
10
|
+
require_relative "timberline/exceptions"
|
10
11
|
require_relative "timberline/config"
|
11
12
|
require_relative "timberline/queue"
|
12
13
|
require_relative "timberline/envelope"
|
13
14
|
|
15
|
+
require_relative "timberline/worker"
|
16
|
+
require_relative "timberline/anonymous_worker"
|
17
|
+
|
18
|
+
# The Timberline class serves as a base namespace for Timberline libraries, but
|
19
|
+
# also provides some convenience methods for accessing queues and quickly and
|
20
|
+
# easily processing items.
|
21
|
+
#
|
14
22
|
class Timberline
|
15
23
|
class << self
|
16
24
|
attr_reader :config
|
17
25
|
attr_accessor :watch_proc
|
18
26
|
end
|
19
27
|
|
28
|
+
# Update the redis server that Timberline uses for its connections.
|
29
|
+
#
|
30
|
+
# If Timberline has not already been configured, this method will initialize
|
31
|
+
# a new Timberline::Config first.
|
32
|
+
#
|
33
|
+
# @param [Redis, Redis::Namespace, nil] server if Redis, wraps it in a namespace.
|
34
|
+
# if Redis::Namespace, uses that namespace directly. If nil, clears out any reference
|
35
|
+
# to the existing redis server.
|
36
|
+
#
|
37
|
+
# @raise [StandardError] if server is not an instance of Redis, Redis::Namespace, or nil.
|
38
|
+
#
|
20
39
|
def self.redis=(server)
|
21
40
|
initialize_if_necessary
|
22
41
|
if server.is_a? Redis
|
@@ -30,6 +49,15 @@ class Timberline
|
|
30
49
|
end
|
31
50
|
end
|
32
51
|
|
52
|
+
# Obtain a reference to the redis connection that Timberline is using.
|
53
|
+
#
|
54
|
+
# If Timberline has not already been configured, this method will initialize
|
55
|
+
# a new Timberline::Config first.
|
56
|
+
#
|
57
|
+
# If a Redis connection has not yet been established, this method will establish one.
|
58
|
+
#
|
59
|
+
# @return [Redis::Namespace]
|
60
|
+
#
|
33
61
|
def self.redis
|
34
62
|
initialize_if_necessary
|
35
63
|
if @redis.nil?
|
@@ -39,107 +67,93 @@ class Timberline
|
|
39
67
|
@redis
|
40
68
|
end
|
41
69
|
|
70
|
+
# @return [Array<Timberline::Queue>] a list of all non-hidden queues for this
|
71
|
+
# instance of Timberline
|
42
72
|
def self.all_queues
|
43
73
|
Timberline.redis.smembers("timberline_queue_names").map { |name| queue(name) }
|
44
74
|
end
|
45
75
|
|
76
|
+
# Convenience method to create a new Queue object
|
77
|
+
# @see Timberline::Queue#initialize
|
46
78
|
def self.queue(queue_name, opts = {})
|
47
79
|
Queue.new(queue_name, opts)
|
48
80
|
end
|
49
81
|
|
82
|
+
# Convenience method to push an item onto a queue
|
83
|
+
# @see Timberline::Queue#push
|
50
84
|
def self.push(queue_name, data, metadata={})
|
51
85
|
queue(queue_name).push(data, metadata)
|
52
86
|
end
|
53
87
|
|
88
|
+
# Convenience method to retry a queue item
|
89
|
+
# @see Timberline::Queue#retry_item
|
54
90
|
def self.retry_item(item)
|
55
91
|
origin_queue = queue(item.origin_queue)
|
56
92
|
origin_queue.retry_item(item)
|
57
93
|
end
|
58
94
|
|
95
|
+
# Convenience method to error out a queue item
|
96
|
+
# @see Timberline::Queue#error_item
|
59
97
|
def self.error_item(item)
|
60
98
|
origin_queue = queue(item.origin_queue)
|
61
99
|
origin_queue.error_item(item)
|
62
100
|
end
|
63
101
|
|
102
|
+
# Convenience method to pause a Queue by name.
|
103
|
+
# @see Timberline::Queue#pause
|
64
104
|
def self.pause(queue_name)
|
65
105
|
queue(queue_name).pause
|
66
106
|
end
|
67
107
|
|
108
|
+
# Convenience method to unpause a Queue by name.
|
109
|
+
# @see Timberline::Queue#unpause
|
68
110
|
def self.unpause(queue_name)
|
69
111
|
queue(queue_name).unpause
|
70
112
|
end
|
71
113
|
|
114
|
+
# Method for providing custom configuration by yielding the config object.
|
115
|
+
# Lazy-loads the Timberline configuration.
|
116
|
+
# @param [Block] block a block that accepts and manipulates a Timberline::Config
|
117
|
+
#
|
72
118
|
def self.configure(&block)
|
73
119
|
initialize_if_necessary
|
74
120
|
yield @config
|
75
121
|
end
|
76
122
|
|
123
|
+
# Lazy-loads the Timberline configuration.
|
124
|
+
# @return [Integer] the maximum number of retries
|
77
125
|
def self.max_retries
|
78
126
|
initialize_if_necessary
|
79
127
|
@config.max_retries
|
80
128
|
end
|
81
129
|
|
130
|
+
# Lazy-loads the Timberline configuration.
|
131
|
+
# @return [Integer] the stat_timeout expressed in minutes
|
82
132
|
def self.stat_timeout
|
83
133
|
initialize_if_necessary
|
84
134
|
@config.stat_timeout
|
85
135
|
end
|
86
136
|
|
137
|
+
# Lazy-loads the Timberline configuration.
|
138
|
+
# @return [Integer] the stat_timeout expressed in seconds
|
87
139
|
def self.stat_timeout_seconds
|
88
140
|
initialize_if_necessary
|
89
141
|
@config.stat_timeout * 60
|
90
142
|
end
|
91
143
|
|
144
|
+
# Create and start a new AnonymousWorker with the given
|
145
|
+
# queue_name and block. Convenience method.
|
146
|
+
#
|
147
|
+
# @param [String] queue_name the name of the queue to watch.
|
148
|
+
# @param [Block] block the block to execute for each queue item
|
149
|
+
# @see Timberline::AnonymousWorker#watch
|
150
|
+
#
|
92
151
|
def self.watch(queue_name, &block)
|
93
|
-
|
94
|
-
while(self.watch?)
|
95
|
-
item = queue.pop
|
96
|
-
fix_binding(block)
|
97
|
-
item.started_processing_at = Time.now.to_f
|
98
|
-
|
99
|
-
begin
|
100
|
-
block.call(item, self)
|
101
|
-
rescue ItemRetried
|
102
|
-
queue.add_retry_stat(item)
|
103
|
-
rescue ItemErrored
|
104
|
-
queue.add_error_stat(item)
|
105
|
-
else
|
106
|
-
item.finished_processing_at = Time.now.to_f
|
107
|
-
queue.add_success_stat(item)
|
108
|
-
end
|
109
|
-
end
|
152
|
+
Timberline::AnonymousWorker.new(queue_name, &block).watch
|
110
153
|
end
|
111
154
|
|
112
|
-
|
155
|
+
private
|
113
156
|
def self.initialize_if_necessary
|
114
157
|
@config ||= Config.new
|
115
158
|
end
|
116
|
-
|
117
|
-
# Hacky-hacky. I like the idea of calling retry_item(item) and
|
118
|
-
# error_item(item)
|
119
|
-
# directly from the watch block, but this seems ugly. There may be a better
|
120
|
-
# way to do this.
|
121
|
-
def self.fix_binding(block)
|
122
|
-
binding = block.binding
|
123
|
-
binding.eval <<-HERE
|
124
|
-
def retry_item(item)
|
125
|
-
Timberline.retry_item(item)
|
126
|
-
raise Timberline::ItemRetried
|
127
|
-
end
|
128
|
-
|
129
|
-
def error_item(item)
|
130
|
-
Timberline.error_item(item)
|
131
|
-
raise Timberline::ItemErrored
|
132
|
-
end
|
133
|
-
HERE
|
134
|
-
end
|
135
|
-
|
136
|
-
def self.watch?
|
137
|
-
watch_proc.nil? ? true : watch_proc.call
|
138
|
-
end
|
139
|
-
|
140
|
-
class ItemRetried < Exception
|
141
|
-
end
|
142
|
-
|
143
|
-
class ItemErrored < Exception
|
144
|
-
end
|
145
159
|
end
|
data/spec/lib/timberline_spec.rb
CHANGED
@@ -316,45 +316,4 @@ describe Timberline do
|
|
316
316
|
end
|
317
317
|
end
|
318
318
|
end
|
319
|
-
|
320
|
-
describe ".watch" do
|
321
|
-
let(:queue) { Timberline.queue("test_queue") }
|
322
|
-
let(:queue_name) { queue.queue_name }
|
323
|
-
let(:success_item) { Timberline::Envelope.from_json(Timberline.redis.xmembers(queue.attr("success_stats")).first) }
|
324
|
-
let(:retried_item) { Timberline::Envelope.from_json(Timberline.redis.xmembers(queue.attr("retry_stats")).first) }
|
325
|
-
let(:errored_item) { Timberline::Envelope.from_json(Timberline.redis.xmembers(queue.attr("error_stats")).first) }
|
326
|
-
|
327
|
-
before do
|
328
|
-
# Make sure that the watch doesn't run forever.
|
329
|
-
Timberline.watch_proc = lambda { queue.length > 0 }
|
330
|
-
Timberline.push(queue_name, "Hey There!")
|
331
|
-
end
|
332
|
-
|
333
|
-
context "If the item can be processed successfully" do
|
334
|
-
it "logs the success of the item" do
|
335
|
-
expect_any_instance_of(Timberline::Queue).to receive(:add_success_stat)
|
336
|
-
Timberline.watch(queue_name) do |item|
|
337
|
-
# don't do anything
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
context "If the item is retried" do
|
343
|
-
it "logs that the item was retried" do
|
344
|
-
expect_any_instance_of(Timberline::Queue).to receive(:add_retry_stat)
|
345
|
-
Timberline.watch(queue_name) do |item|
|
346
|
-
raise Timberline::ItemRetried
|
347
|
-
end
|
348
|
-
end
|
349
|
-
end
|
350
|
-
|
351
|
-
context "If the item can be processed successfully" do
|
352
|
-
it "logs that the item was retried" do
|
353
|
-
expect_any_instance_of(Timberline::Queue).to receive(:add_error_stat)
|
354
|
-
Timberline.watch(queue_name) do |item|
|
355
|
-
raise Timberline::ItemErrored
|
356
|
-
end
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
319
|
end
|
data/timberline.gemspec
CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.add_runtime_dependency "trollop"
|
25
25
|
s.add_runtime_dependency "daemons"
|
26
26
|
|
27
|
+
s.add_development_dependency "yard"
|
27
28
|
s.add_development_dependency "rake"
|
28
29
|
s.add_development_dependency "rspec", '~> 3.0.0.rc1'
|
29
30
|
s.add_development_dependency "pry"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timberline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tommy Morgan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: yard
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rake
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -137,16 +151,23 @@ files:
|
|
137
151
|
- .rspec
|
138
152
|
- .ruby-gemset
|
139
153
|
- .travis.yml
|
154
|
+
- .yardoc/checksums
|
155
|
+
- .yardoc/object_types
|
156
|
+
- .yardoc/objects/root.dat
|
157
|
+
- .yardoc/proxy_types
|
140
158
|
- CHANGELOG
|
141
159
|
- Gemfile
|
142
160
|
- README.markdown
|
143
161
|
- Rakefile
|
144
162
|
- bin/timberline
|
145
163
|
- lib/timberline.rb
|
164
|
+
- lib/timberline/anonymous_worker.rb
|
146
165
|
- lib/timberline/config.rb
|
147
166
|
- lib/timberline/envelope.rb
|
167
|
+
- lib/timberline/exceptions.rb
|
148
168
|
- lib/timberline/queue.rb
|
149
169
|
- lib/timberline/version.rb
|
170
|
+
- lib/timberline/worker.rb
|
150
171
|
- spec/config/test_config.yaml
|
151
172
|
- spec/lib/timberline/config_spec.rb
|
152
173
|
- spec/lib/timberline/envelope_spec.rb
|
@@ -181,3 +202,4 @@ specification_version: 4
|
|
181
202
|
summary: Timberline is a simple and extensible queuing system built in Ruby and backed
|
182
203
|
by Redis.
|
183
204
|
test_files: []
|
205
|
+
has_rdoc:
|