timberline 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in treeline.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,214 @@
1
+ # Timberline
2
+
3
+ ## Purpose
4
+
5
+ Timberline is a dead-simple queuing service written in Ruby and backed by Redis.
6
+ It makes a few assumptions about how you'd like to handle your queues and what
7
+ kind of issues you might be dealing with:
8
+
9
+ 1. Timberline assumes that you want to be able to programmatically retry some
10
+ failed jobs, and that you want to keep track of jobs that totally errored out
11
+ so that you can try them again.
12
+
13
+ 2. Timberline assumes that you want to queue data, and not actions. You can have
14
+ one app that puts data onto the queue and another app that reads data from
15
+ the queue, and the only thing they have to have in common (aside from knowing
16
+ what the data means) is that they both include Timberline.
17
+
18
+ 3. Timberline assumes that it's preferable, if not important to you, to process
19
+ jobs as fast as you possibly can. To that end, Timberline uses blocking reads
20
+ in Redis to pull jobs off of the queue as soon as they're available.
21
+
22
+ ## Concepts
23
+
24
+ ### Retries
25
+
26
+ Sometimes jobs just fail because of something that was outside of your control.
27
+ Maybe there was a glitch and your HTTP connection to PayPal didn't go through,
28
+ or maybe Github is down right now, or... whatever. In these situations it makes
29
+ sense to re-queue jobs and let them retry - just don't let them do it forever or
30
+ they may never leave. Timberline is designed to make retrying jobs in these
31
+ circumstances super-easy.
32
+
33
+ ### Errors
34
+
35
+ On the other hand, sometimes your jobs deserved to fail. Maybe there was a bug
36
+ in your processor code, or maybe a user was able to sneak bad data past you. In
37
+ any event, Timberline maintains an error queue where jobs go when they're
38
+ explicitly marked as bad jobs, or when they've been retried the maximum number
39
+ of times. You can then check the jobs out and resubmit them to their original
40
+ queue after you fix the issue.
41
+
42
+ ### The Envelope
43
+
44
+ Sounds SOAPy, I know. The envelope is a simple object that wraps the data you
45
+ want to put on the queue - it's responsible for tracking things like the job ID,
46
+ the queue it was put on, how many times it's been retried, etc., etc. It's also
47
+ accessible to both the queue processor and whatever is putting jobs on the
48
+ queue, so if you want to be able to check in on the administrative details (or
49
+ add some of your own) this is a great place to do it instead of muddying up the
50
+ meat of your message.
51
+
52
+ ## Usage
53
+
54
+ Timberline is designed to be as easy to work with as possible, and operates almost
55
+ like a DSL for interacting with stuff on the queue.
56
+
57
+ ### Configuration
58
+
59
+ There are a few things that you probably want to be able to configure in
60
+ Timberline. At the moment this is largely stuff related to the redis server
61
+ connection, but you can also configure a namespace for your redis queues
62
+ (defaults to "timberline") and a maximum number of retry attempts for jobs in the
63
+ queue (more on that later). There are 3 ways to configure Timberline:
64
+
65
+ 1. The most direct way is to configure Timberline via ruby code as follows:
66
+
67
+ Timberline.config do |c|
68
+ c.database = 1
69
+ c.host = "192.168.1.105"
70
+ c.port = 12345
71
+ c.password = "foobar"
72
+ end
73
+
74
+ ...As long as you run this block before you attempt to access your queues,
75
+ your settings will all take effect. Redis defaults will be used if you omit
76
+ anything.
77
+
78
+ 2. If you're including Timberline in a Rails app, there's a convenient way to
79
+ configure it that should fit in with the rest of your app - if you include a
80
+ yaml file named timberline.yaml in your config directory, Timberline will
81
+ automatically detect it and load it up. The syntax for this file is
82
+ shockingly boring:
83
+
84
+ database: 1
85
+ host: 192.168.1.105
86
+ port: 12345
87
+ password: foobar
88
+
89
+ 3. Like the yaml format but you're not using Rails? Don't worry, just write your
90
+ yaml file and set the TIMBERLINE\_YAML constant inside your app like so:
91
+
92
+ TIMBERLINE_YAML = 'path/to/your/yaml/file.yaml'
93
+
94
+ ### Pushing jobs onto a queue
95
+
96
+ To push a job onto the queue you'll want to make use of the `Timberline#push`
97
+ method, like so:
98
+
99
+ Timberline.push "queue_name", data, { :other_data => some_stuff }
100
+
101
+ `queue_name` is the name of the queue you want to push data onto; data is the
102
+ data you want to push onto the queue (remember that this all gets converted to
103
+ JSON, so you probably want to stick to things that represent well as strings),
104
+ and the optional third argument is a hash of any extra parameters you want to
105
+ include in the job's envelope.
106
+
107
+ ### Reading from a queue
108
+
109
+ Reading from a queue is pretty simple in Timberline. You can simply write
110
+ something like the following:
111
+
112
+ Timberline.watch "queue_name" do |job|
113
+ begin
114
+ puts job.other_data
115
+ doSomethingWithThisStuff(job.contents)
116
+ rescue SomeTransientError
117
+ retry_job(job)
118
+ rescue SomeFatalError
119
+ error_job(job)
120
+ end
121
+ end
122
+
123
+ You will, in all likelihood, be writing more complicated stuff than this, of
124
+ course. But you call Timberline.watch and provide it with a queue name and a block
125
+ that will be called for each job as Timberline reads them off of the queue. Things
126
+ to note:
127
+
128
+ - The variable that will be passed into the block is the envelope for the job.
129
+ To read what you actually posted into the queue, use job.contents.
130
+ - The envelope makes use of method\_missing to give you easy access to your
131
+ metadata (note that we used job.other\_data to access the other\_data property
132
+ that we added in the pushing example).
133
+ - retry\_job and error\_job are exactly what they seem like - they either try to
134
+ retry the job in the event of a transient error, or put it on the error queue
135
+ for processing if a more fatal error occurs.
136
+
137
+ ### The error queue
138
+
139
+ If you want to interact with the error queue directly, it's accessible via
140
+ `Timberline#error_queue`. You can pop items directly off of the queue to operate
141
+ on them if you want, or you could write a queue processor that reads off of that
142
+ queue (its queue name should always be "Timberline\_errors").
143
+
144
+ ### Using the binary
145
+
146
+ In order to make reading off of the queue easier, there's a binary named
147
+ `Timberline` included with this gem.
148
+
149
+ Example:
150
+
151
+ # timberline_sample.rb
152
+ TIMBERLINE_YAML = "timberline_sample.yaml"
153
+
154
+ watch "sample_queue" do |job|
155
+ puts job.contents
156
+ end
157
+
158
+ The above file, when executed via `timberline timberline_sample.rb`, will print out
159
+ the value of any object put on the queue. If no objects are on the queue it will
160
+ block until either the process is killed, or until something is added to the
161
+ queue.
162
+
163
+ There are some options to the Timberline binary that you may find helpful -
164
+ `timberline --help` for more.
165
+
166
+ ## TODO
167
+
168
+ Still to be done:
169
+
170
+ - A simple Sinatra interface for monitoring the statuses of queues and
171
+ observing/resubmitting errored-out jobs.
172
+ - Binary updates - the binary should probably fork processes for each job
173
+ that it tries to process so that it's more robust.
174
+ - DSL improvements - the DSL-ish setup for Timberline could probably use some
175
+ updates to be both more obvious and easier to use.
176
+ - Documentation - need to get YARD docs added so that the API is more completely
177
+ documented. For the time being, though, there are some fairly comprehensive
178
+ test suites.
179
+ - Timing - it would be crazy useful to be able to automatically log per-queue
180
+ statistics about how long jobs are taking. Definitely something like an "over
181
+ the last 5 minutes/past 1000 jobs" stat would be useful, but we may also be
182
+ interested in some kind of lifetime average.
183
+
184
+ ## Future
185
+
186
+ Stuff that would be cool but isn't quite on the radar yet:
187
+
188
+ - Client libraries for other languages - maybe you want to put stuff onto the
189
+ queue using Ruby but read from it in, say, Java. Or vice-versa. Or whatever.
190
+ The queue data should be platform-agnostic.
191
+
192
+ ## Contributions
193
+
194
+ If Timberline interests you and you think you might want to contribute, hit me up
195
+ over Github. You can also just fork it and make some changes, but there's a
196
+ better chance that your work won't be duplicated or rendered obsolete if you
197
+ check in on the current development status first.
198
+
199
+ ## Development notes
200
+
201
+ You need Redis installed to do development on Timberline, and currently the test
202
+ suites assume that you're using the default configurations for Redis. That
203
+ should probably change, but it probably won't until someone needs it to.
204
+
205
+ Gem requirements/etc. should be handled by Bundler.
206
+
207
+ ## License
208
+ Copyright (C) 2012 by Tommy Morgan
209
+
210
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
211
+
212
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
213
+
214
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/unit/test_*.rb']
7
+ t.verbose = true
8
+ t.warning = true
9
+ end
data/bin/timberline ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'daemons'
4
+ require 'trollop'
5
+
6
+ require 'timberline'
7
+
8
+ opts = Trollop::options do
9
+ version "timberline #{Timberline::VERSION} (c) 2012 Tommy Morgan"
10
+ banner <<-EOS
11
+ The timberline command-line interface allows you to easily start up Timberline queue watchers.
12
+
13
+ Usage:
14
+ timberline [options] <filename>
15
+ where [options] are:
16
+ EOS
17
+
18
+ opt :daemonize, "Run the queue listener as a daemon", :default => false
19
+ opt :log_output, "log the output to <filename>.output (only applicable in conjunction with the daemonize option)", :default => false
20
+ opt :config, "YAML config file to set up Timberline options",
21
+ :type => String, :default => nil
22
+ end
23
+
24
+ unless opts[:config].nil?
25
+ TIMBERLINE_YAML = opts[:config]
26
+ end
27
+
28
+ timberline_file = ARGV[0]
29
+ timberline_spec = File.read(timberline_file)
30
+
31
+ if opts[:daemonize]
32
+ puts "Entering daemon mode"
33
+ Daemons.daemonize({ :app_name => timberline_file, :log_output => opts[:log_output] })
34
+ end
35
+
36
+ puts "Listening..."
37
+ Timberline.class_eval(timberline_spec)
@@ -0,0 +1,45 @@
1
+ class Timberline
2
+ class Config
3
+ attr_accessor :database, :host, :port, :timeout, :password, :logger, :namespace, :max_retries
4
+
5
+ def initialize
6
+ if defined? TIMBERLINE_YAML
7
+ if File.exists?(TIMBERLINE_YAML)
8
+ load_from_yaml(TIMBERLINE_YAML)
9
+ else
10
+ raise "Specified Timberline config file #{TIMBERLINE_YAML} is not present."
11
+ end
12
+ elsif defined? RAILS_ROOT
13
+ config_file = File.join(RAILS_ROOT, 'config', 'timberline.yaml')
14
+ if File.exists?(config_file)
15
+ load_from_yaml(config_file)
16
+ end
17
+ end
18
+ end
19
+
20
+ def namespace
21
+ @namespace ||= 'timberline'
22
+ end
23
+
24
+ def max_retries
25
+ @max_retries ||= 5
26
+ end
27
+
28
+ def redis_config
29
+ config = {}
30
+
31
+ { :db => database, :host => host, :port => port, :timeout => timeout, :password => password, :logger => logger }.each do |name, value|
32
+ config[name] = value unless value.nil?
33
+ end
34
+
35
+ config
36
+ end
37
+
38
+ def load_from_yaml(filename)
39
+ yaml_config = YAML.load_file(filename)
40
+ ["database","host","port","timeout","password","logger","namespace"].each do |setting|
41
+ self.instance_variable_set("@#{setting}", yaml_config[setting])
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ class Timberline
2
+ class Envelope
3
+
4
+ def self.from_json(json_string)
5
+ envelope = Envelope.new
6
+ envelope.instance_variable_set("@metadata", JSON.parse(json_string))
7
+ envelope.contents = envelope.metadata.delete("contents")
8
+ envelope
9
+ end
10
+
11
+ attr_accessor :contents
12
+ attr_reader :metadata
13
+
14
+ def initialize
15
+ @metadata = {}
16
+ end
17
+
18
+ def to_s
19
+ raise MissingContentException if contents.nil? || contents.empty?
20
+
21
+ JSON.unparse(build_envelope_hash)
22
+ end
23
+
24
+ def method_missing(method_name, *args)
25
+ method_name = method_name.to_s
26
+ if method_name[-1] == "="
27
+ assign_var(method_name[0, method_name.size - 1], args.first)
28
+ else
29
+ return metadata[method_name]
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def build_envelope_hash
36
+ { :contents => contents }.merge(@metadata)
37
+ end
38
+
39
+ def assign_var(name, value)
40
+ @metadata[name] = value
41
+ end
42
+
43
+ end
44
+
45
+ class MissingContentException < Exception
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ class Timberline
2
+ class Queue
3
+ attr_reader :queue_name, :read_timeout
4
+
5
+ def initialize(queue_name, read_timeout= 0)
6
+ @queue_name = queue_name
7
+ @read_timeout = read_timeout
8
+ @redis = Timberline.redis
9
+ end
10
+
11
+ def length
12
+ @redis.llen @queue_name
13
+ end
14
+
15
+ def pop
16
+ br_tuple = @redis.brpop(@queue_name, read_timeout)
17
+ envelope_string = br_tuple.nil? ? nil : br_tuple[1]
18
+ if envelope_string.nil?
19
+ nil
20
+ else
21
+ Envelope.from_json(envelope_string)
22
+ end
23
+ end
24
+
25
+ def push(item)
26
+ case item
27
+ when Envelope
28
+ @redis.lpush @queue_name, item
29
+ else
30
+ @redis.lpush @queue_name, wrap(item)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def wrap(item)
37
+ envelope = Envelope.new
38
+ envelope.contents = item
39
+ envelope
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,84 @@
1
+ class Timberline
2
+ class QueueManager
3
+ attr_reader :queue_list
4
+
5
+ def initialize
6
+ @queue_list = {}
7
+ end
8
+
9
+ def error_queue
10
+ @error_queue ||= Queue.new("timberline_errors")
11
+ end
12
+
13
+ def queue(queue_name)
14
+ if @queue_list[queue_name].nil?
15
+ @queue_list[queue_name] = Queue.new(queue_name)
16
+ end
17
+ @queue_list[queue_name]
18
+ end
19
+
20
+ def push(queue_name, data, metadata={})
21
+ data = wrap(data, metadata.merge({:origin_queue => queue_name}))
22
+ queue(queue_name).push(data)
23
+ end
24
+
25
+ def retry_job(job)
26
+ if (job.retries < Timberline.max_retries)
27
+ job.retries += 1
28
+ job.last_tried_at = DateTime.now
29
+ queue(job.origin_queue).push(job)
30
+ else
31
+ error_job(job)
32
+ end
33
+ end
34
+
35
+ def error_job(job)
36
+ job.fatal_error_at = DateTime.now
37
+ error_queue.push(job)
38
+ end
39
+
40
+ def watch(queue_name, &block)
41
+ queue = queue(queue_name)
42
+ while(true)
43
+ job = queue.pop
44
+ fix_binding(block)
45
+ block.call(job, self)
46
+ end
47
+ end
48
+
49
+ private
50
+ def wrap(contents, metadata)
51
+ env = Envelope.new
52
+ env.contents = contents
53
+ metadata.each do |key, value|
54
+ env.send("#{key.to_s}=", value)
55
+ end
56
+
57
+ env.job_id = next_id_for_queue(metadata[:origin_queue])
58
+ env.retries = 0
59
+ env.submitted_at = DateTime.now
60
+
61
+ env
62
+ end
63
+
64
+ def next_id_for_queue(queue_name)
65
+ Timberline.redis.incr "#{queue_name}_id_seq"
66
+ end
67
+
68
+ # Hacky-hacky. I like the idea of calling retry_job(job) and error_job(job)
69
+ # directly from the watch block, but this seems ugly. There may be a better
70
+ # way to do this.
71
+ def fix_binding(block)
72
+ binding = block.binding
73
+ binding.eval <<-HERE
74
+ def retry_job(job)
75
+ Timberline.retry_job(job)
76
+ end
77
+
78
+ def error_job(job)
79
+ Timberline.error_job(job)
80
+ end
81
+ HERE
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ class Timberline
2
+ VERSION = "0.1.2"
3
+ end
data/lib/timberline.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'forwardable'
2
+ require 'json'
3
+ require 'logger'
4
+ require 'yaml'
5
+
6
+ require 'redis'
7
+ require 'redis-namespace'
8
+
9
+ require_relative "timberline/version"
10
+ require_relative "timberline/config"
11
+ require_relative "timberline/queue"
12
+ require_relative "timberline/envelope"
13
+ require_relative "timberline/queue_manager"
14
+
15
+ class Timberline
16
+ class << self
17
+ extend Forwardable
18
+
19
+ def_delegators :@queue_manager, :error_job, :retry_job, :watch, :push
20
+
21
+ attr_reader :config
22
+
23
+ def redis=(server)
24
+ initialize_if_necessary
25
+ if server.is_a? Redis
26
+ @redis = Redis::Namespace.new(@config.namespace, :redis => server)
27
+ elsif server.is_a? Redis::Namespace
28
+ @redis = server
29
+ elsif server.nil?
30
+ @redis = nil
31
+ else
32
+ raise "Not a valid Redis connection."
33
+ end
34
+ end
35
+
36
+ def redis
37
+ initialize_if_necessary
38
+ if @redis.nil?
39
+ self.redis = Redis.new(@config.redis_config)
40
+ end
41
+ @redis
42
+ end
43
+
44
+ def config(&block)
45
+ initialize_if_necessary
46
+ yield @config
47
+ end
48
+
49
+ def max_retries
50
+ initialize_if_necessary
51
+ @config.max_retries
52
+ end
53
+
54
+ private
55
+ # Don't know if I like doing this, but we want the configuration to be
56
+ # lazy-loaded so as to be sure and give users a chance to set up their
57
+ # configurations.
58
+ def initialize_if_necessary
59
+ @config ||= Config.new
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+ Timberline.instance_variable_set("@queue_manager", Timberline::QueueManager.new)
@@ -0,0 +1,6 @@
1
+ host: localhost
2
+ port: 12345
3
+ timeout: 10
4
+ password: foo
5
+ database: 3
6
+ namespace: treecurve