timberline 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -5,5 +5,4 @@ Rake::TestTask.new do |t|
5
5
  t.libs << "test"
6
6
  t.test_files = FileList['test/unit/test_*.rb']
7
7
  t.verbose = true
8
- t.warning = true
9
8
  end
@@ -1,22 +1,18 @@
1
- require 'forwardable'
2
1
  require 'json'
3
2
  require 'logger'
4
3
  require 'yaml'
5
4
 
6
5
  require 'redis'
7
6
  require 'redis-namespace'
7
+ require 'redis-expiring-set/monkeypatch'
8
8
 
9
9
  require_relative "timberline/version"
10
10
  require_relative "timberline/config"
11
11
  require_relative "timberline/queue"
12
12
  require_relative "timberline/envelope"
13
- require_relative "timberline/queue_manager"
14
13
 
15
14
  class Timberline
16
15
  class << self
17
- extend Forwardable
18
-
19
- def_delegators :@queue_manager, :error_job, :retry_job, :watch, :push
20
16
 
21
17
  attr_reader :config
22
18
 
@@ -38,9 +34,54 @@ class Timberline
38
34
  if @redis.nil?
39
35
  self.redis = Redis.new(@config.redis_config)
40
36
  end
37
+
41
38
  @redis
42
39
  end
43
40
 
41
+ def all_queues
42
+ Timberline.redis.smembers("timberline_queue_names").map { |name| queue(name) }
43
+ end
44
+
45
+ def error_queue
46
+ @error_queue ||= Queue.new("timberline_errors")
47
+ end
48
+
49
+ def queue(queue_name)
50
+ if @queue_list[queue_name].nil?
51
+ @queue_list[queue_name] = Queue.new(queue_name)
52
+ end
53
+ @queue_list[queue_name]
54
+ end
55
+
56
+ def push(queue_name, data, metadata={})
57
+ queue(queue_name).push(data, metadata)
58
+ end
59
+
60
+ def retry_item(item)
61
+ if (item.retries < max_retries)
62
+ item.retries += 1
63
+ item.last_tried_at = Time.now.to_f
64
+ queue(item.origin_queue).push(item)
65
+ raise ItemRetried
66
+ else
67
+ error_item(item)
68
+ end
69
+ end
70
+
71
+ def error_item(item)
72
+ item.fatal_error_at = Time.now.to_f
73
+ error_queue.push(item)
74
+ raise ItemErrored
75
+ end
76
+
77
+ def pause(queue_name)
78
+ queue(queue_name).pause
79
+ end
80
+
81
+ def unpause(queue_name)
82
+ queue(queue_name).unpause
83
+ end
84
+
44
85
  def config(&block)
45
86
  initialize_if_necessary
46
87
  yield @config
@@ -51,6 +92,36 @@ class Timberline
51
92
  @config.max_retries
52
93
  end
53
94
 
95
+ def stat_timeout
96
+ initialize_if_necessary
97
+ @config.stat_timeout
98
+ end
99
+
100
+ def stat_timeout_seconds
101
+ initialize_if_necessary
102
+ @config.stat_timeout * 60
103
+ end
104
+
105
+ def watch(queue_name, &block)
106
+ queue = queue(queue_name)
107
+ while(true)
108
+ item = queue.pop
109
+ fix_binding(block)
110
+ item.started_processing_at = Time.now.to_f
111
+
112
+ begin
113
+ block.call(item, self)
114
+ rescue ItemRetried
115
+ queue.add_retry_stat(item)
116
+ rescue ItemErrored
117
+ queue.add_error_stat(item)
118
+ else
119
+ item.finished_processing_at = Time.now.to_f
120
+ queue.add_success_stat(item)
121
+ end
122
+ end
123
+ end
124
+
54
125
  private
55
126
  # Don't know if I like doing this, but we want the configuration to be
56
127
  # lazy-loaded so as to be sure and give users a chance to set up their
@@ -58,8 +129,32 @@ class Timberline
58
129
  def initialize_if_necessary
59
130
  @config ||= Config.new
60
131
  end
132
+
133
+ # Hacky-hacky. I like the idea of calling retry_item(item) and
134
+ # error_item(item)
135
+ # directly from the watch block, but this seems ugly. There may be a better
136
+ # way to do this.
137
+ def fix_binding(block)
138
+ binding = block.binding
139
+ binding.eval <<-HERE
140
+ def retry_item(item)
141
+ Timberline.retry_item(item)
142
+ raise Timberline::ItemRetried
143
+ end
144
+
145
+ def error_item(item)
146
+ Timberline.error_item(item)
147
+ raise Timberline::ItemErrored
148
+ end
149
+ HERE
150
+ end
151
+ end
152
+
153
+ class ItemRetried < Exception
61
154
  end
62
155
 
156
+ class ItemErrored < Exception
157
+ end
63
158
  end
64
159
 
65
- Timberline.instance_variable_set("@queue_manager", Timberline::QueueManager.new)
160
+ Timberline.instance_variable_set("@queue_list", {})
@@ -1,6 +1,6 @@
1
1
  class Timberline
2
2
  class Config
3
- attr_accessor :database, :host, :port, :timeout, :password, :logger, :namespace, :max_retries
3
+ attr_accessor :database, :host, :port, :timeout, :password, :logger, :namespace, :max_retries, :stat_timeout
4
4
 
5
5
  def initialize
6
6
  if defined? TIMBERLINE_YAML
@@ -15,14 +15,11 @@ class Timberline
15
15
  load_from_yaml(config_file)
16
16
  end
17
17
  end
18
- end
19
-
20
- def namespace
21
- @namespace ||= 'timberline'
22
- end
23
18
 
24
- def max_retries
19
+ # load defaults
20
+ @namespace ||= 'timberline'
25
21
  @max_retries ||= 5
22
+ @stat_timeout ||= 60
26
23
  end
27
24
 
28
25
  def redis_config
@@ -6,6 +6,15 @@ class Timberline
6
6
  @queue_name = queue_name
7
7
  @read_timeout = read_timeout
8
8
  @redis = Timberline.redis
9
+ @redis.sadd "timberline_queue_names", queue_name
10
+ end
11
+
12
+ def delete!
13
+ @redis.del @queue_name
14
+ @redis.keys("#{@queue_name}:*").each do |key|
15
+ @redis.del key
16
+ end
17
+ @redis.srem "timberline_queue_names", @queue_name
9
18
  end
10
19
 
11
20
  def length
@@ -13,6 +22,8 @@ class Timberline
13
22
  end
14
23
 
15
24
  def pop
25
+ block_while_paused
26
+
16
27
  br_tuple = @redis.brpop(@queue_name, read_timeout)
17
28
  envelope_string = br_tuple.nil? ? nil : br_tuple[1]
18
29
  if envelope_string.nil?
@@ -22,21 +33,91 @@ class Timberline
22
33
  end
23
34
  end
24
35
 
25
- def push(item)
26
- case item
36
+ def push(contents, metadata = {})
37
+ case contents
27
38
  when Envelope
28
- @redis.lpush @queue_name, item
39
+ @redis.lpush @queue_name, contents
29
40
  else
30
- @redis.lpush @queue_name, wrap(item)
41
+ @redis.lpush @queue_name, wrap(contents, metadata)
31
42
  end
32
43
  end
33
44
 
45
+ def pause
46
+ @redis[attr("paused")] = "true"
47
+ end
48
+
49
+ def unpause
50
+ @redis[attr("paused")] = "false"
51
+ end
52
+
53
+ def paused?
54
+ @redis[attr("paused")] == "true"
55
+ end
56
+
57
+ def attr(key)
58
+ "#{@queue_name}:#{key}"
59
+ end
60
+
61
+ def number_errors
62
+ Timberline.redis.xcard attr("error_stats")
63
+ end
64
+
65
+ def number_retries
66
+ Timberline.redis.xcard attr("retry_stats")
67
+ end
68
+
69
+ def number_successes
70
+ Timberline.redis.xcard attr("success_stats")
71
+ end
72
+
73
+ def average_execution_time
74
+ successes = Timberline.redis.xmembers(attr("success_stats")).map { |item| Envelope.from_json(item)}
75
+ times = successes.map { |item| item.finished_processing_at.to_f - item.started_processing_at.to_f }
76
+ times.inject(0, :+) / times.size.to_f
77
+ end
78
+
79
+ def add_retry_stat(item)
80
+ add_stat_for_key(attr("retry_stats"), item)
81
+ end
82
+
83
+ def add_error_stat(item)
84
+ add_stat_for_key(attr("success_stats"), item)
85
+ end
86
+
87
+ def add_success_stat(item)
88
+ add_stat_for_key(attr("success_stats"), item)
89
+ end
90
+
34
91
  private
35
92
 
36
- def wrap(item)
93
+ def add_stat_for_key(key, item)
94
+ Timberline.redis.xadd key, item, Time.now + Timberline.stat_timeout_seconds
95
+ end
96
+
97
+ def next_id
98
+ Timberline.redis.incr attr("id_seq")
99
+ end
100
+
101
+ def wrap(contents, metadata)
37
102
  envelope = Envelope.new
38
- envelope.contents = item
103
+ envelope.contents = contents
104
+ metadata.each do |key, value|
105
+ envelope.send("#{key.to_s}=", value)
106
+ end
107
+
108
+ # default metadata
109
+ envelope.item_id = next_id
110
+ envelope.retries = 0
111
+ envelope.submitted_at = Time.now.to_f
112
+ envelope.origin_queue = @queue_name
113
+
39
114
  envelope
40
115
  end
116
+
117
+ def block_while_paused
118
+ while(self.paused?)
119
+ sleep(1)
120
+ end
121
+ end
41
122
  end
42
123
  end
@@ -1,3 +1,3 @@
1
1
  class Timberline
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,10 +1,18 @@
1
1
  require 'test/unit'
2
2
 
3
- require 'partial_minispec'
3
+ require 'shoulda'
4
4
 
5
5
  # include the gem
6
6
  require 'timberline'
7
7
 
8
+ def reset_timberline
9
+ Timberline.redis = nil
10
+ Timberline.instance_variable_set("@config", nil)
11
+ clear_test_db
12
+ Timberline.redis = nil
13
+ Timberline.instance_variable_set("@queue_list", {})
14
+ end
15
+
8
16
  # Use database 15 for testing, so we don't risk overwriting any data that's
9
17
  # actually useful
10
18
  def clear_test_db
@@ -13,3 +21,4 @@ def clear_test_db
13
21
  end
14
22
  Timberline.redis.flushdb
15
23
  end
24
+
@@ -1,12 +1,12 @@
1
1
  require 'test_helper'
2
2
 
3
- describe Timberline::Config do
4
- describe "Without any preset YAML configs" do
5
- before do
3
+ class ConfigTest < Test::Unit::TestCase
4
+ context "Without any preset YAML configs" do
5
+ setup do
6
6
  @config = Timberline::Config.new
7
7
  end
8
8
 
9
- it "builds a proper config hash for Redis" do
9
+ should "build a proper config hash for Redis" do
10
10
  @logger = Logger.new STDERR
11
11
 
12
12
  @config.host = "localhost"
@@ -27,7 +27,7 @@ describe Timberline::Config do
27
27
 
28
28
  end
29
29
 
30
- it "reads configuration from a YAML config file" do
30
+ should "reads configuration from a YAML config file" do
31
31
  base_dir = File.dirname(File.path(__FILE__))
32
32
  yaml_file = File.join(base_dir, "..", "test_config.yaml")
33
33
  @config.load_from_yaml(yaml_file)
@@ -40,34 +40,39 @@ describe Timberline::Config do
40
40
  end
41
41
  end
42
42
 
43
- describe "when in a Rails app without a config file" do
44
- before do
45
- RAILS_ROOT = File.join(File.dirname(File.path(__FILE__)), "..", "gibberish")
43
+ context "when in a Rails app without a config file" do
44
+ setup do
45
+ Object::RAILS_ROOT = File.join(File.dirname(File.path(__FILE__)), "..", "gibberish")
46
46
  @config = Timberline::Config.new
47
47
  end
48
48
 
49
- after do
49
+ teardown do
50
50
  Object.send(:remove_const, :RAILS_ROOT)
51
51
  end
52
52
 
53
- it "should load successfully without any configs." do
54
- ["database","host","port","timeout","password","logger","namespace"].each do |setting|
53
+ should "load successfully without any configs." do
54
+ ["database","host","port","timeout","password","logger"].each do |setting|
55
55
  assert_equal nil, @config.instance_variable_get("@#{setting}")
56
56
  end
57
+
58
+ # check defaults
59
+ assert_equal "timberline", @config.namespace
60
+ assert_equal 5, @config.max_retries
61
+ assert_equal 60, @config.stat_timeout
57
62
  end
58
63
  end
59
64
 
60
- describe "when in a Rails app with a config file" do
61
- before do
62
- RAILS_ROOT = File.join(File.dirname(File.path(__FILE__)), "..", "fake_rails")
65
+ context "when in a Rails app with a config file" do
66
+ setup do
67
+ Object::RAILS_ROOT = File.join(File.dirname(File.path(__FILE__)), "..", "fake_rails")
63
68
  @config = Timberline::Config.new
64
69
  end
65
70
 
66
- after do
71
+ teardown do
67
72
  Object.send(:remove_const, :RAILS_ROOT)
68
73
  end
69
74
 
70
- it "should load the config/timberline.yaml file" do
75
+ should "load the config/timberline.yaml file" do
71
76
  assert_equal "localhost", @config.host
72
77
  assert_equal 12345, @config.port
73
78
  assert_equal 10, @config.timeout
@@ -77,17 +82,17 @@ describe Timberline::Config do
77
82
  end
78
83
  end
79
84
 
80
- describe "when TIMBERLINE_YAML is defined" do
81
- before do
82
- TIMBERLINE_YAML = File.join(File.dirname(File.path(__FILE__)), "..", "test_config.yaml")
85
+ context "when TIMBERLINE_YAML is defined" do
86
+ setup do
87
+ Object::TIMBERLINE_YAML = File.join(File.dirname(File.path(__FILE__)), "..", "test_config.yaml")
83
88
  @config = Timberline::Config.new
84
89
  end
85
90
 
86
- after do
91
+ teardown do
87
92
  Object.send(:remove_const, :TIMBERLINE_YAML)
88
93
  end
89
94
 
90
- it "should load the specified yaml file" do
95
+ should "load the specified yaml file" do
91
96
  assert_equal "localhost", @config.host
92
97
  assert_equal 12345, @config.port
93
98
  assert_equal 10, @config.timeout
@@ -97,16 +102,16 @@ describe Timberline::Config do
97
102
  end
98
103
  end
99
104
 
100
- describe "when TIMBERLINE_YAML is defined, but doesn't exist" do
101
- before do
102
- TIMBERLINE_YAML = File.join(File.dirname(File.path(__FILE__)), "..", "fake_config.yaml")
105
+ context "when TIMBERLINE_YAML is defined, but doesn't exist" do
106
+ setup do
107
+ Object::TIMBERLINE_YAML = File.join(File.dirname(File.path(__FILE__)), "..", "fake_config.yaml")
103
108
  end
104
109
 
105
- after do
110
+ teardown do
106
111
  Object.send(:remove_const, :TIMBERLINE_YAML)
107
112
  end
108
113
 
109
- it "should raise an exception" do
114
+ should "raise an exception" do
110
115
  assert_raises RuntimeError do
111
116
  @config = Timberline::Config.new
112
117
  end
@@ -1,51 +1,53 @@
1
1
  require 'test_helper'
2
2
  require 'date'
3
3
 
4
- describe Timberline::Envelope do
5
- before do
6
- @envelope = Timberline::Envelope.new
7
- end
4
+ class EnvelopeTest < Test::Unit::TestCase
5
+
6
+ context "newly instantiated" do
7
+ setup do
8
+ @envelope = Timberline::Envelope.new
9
+ end
8
10
 
9
- describe "newly instantiated" do
10
- it "raises a MissingContentException when to_s is called because the contents are nil" do
11
+ should "raises a MissingContentException when to_s is called because the contents are nil" do
11
12
  assert_raises Timberline::MissingContentException do
12
13
  @envelope.to_s
13
14
  end
14
15
  end
15
16
 
16
- it "has an empty hash for metadata" do
17
+ should "has an empty hash for metadata" do
17
18
  assert_equal({}, @envelope.metadata)
18
19
  end
19
20
 
20
- it "allows for the reading of attributes via method_missing magic" do
21
+ should "allows for the reading of attributes via method_missing magic" do
21
22
  @envelope.metadata["original_queue"] = "test_queue"
22
23
  assert_equal "test_queue", @envelope.original_queue
23
24
  end
24
25
 
25
- it "allows for the setting of attributes via method_missing magic" do
26
+ should "allows for the setting of attributes via method_missing magic" do
26
27
  @envelope.original_queue = "test_queue"
27
28
  assert_equal "test_queue", @envelope.metadata["original_queue"]
28
29
  end
29
30
  end
30
31
 
31
- describe "with contents" do
32
- before do
32
+ context "with contents" do
33
+ setup do
34
+ @envelope = Timberline::Envelope.new
33
35
  @envelope.contents = "Test data"
34
36
  end
35
37
 
36
- it "returns a JSON string when to_s is called" do
38
+ should "returns a JSON string when to_s is called" do
37
39
  json_string = @envelope.to_s
38
40
  json_data = JSON.parse(json_string)
39
41
  assert_equal "Test data", json_data["contents"]
40
42
  end
41
43
 
42
- it "only includes a 'contents' parameter by default" do
44
+ should "only includes a 'contents' parameter by default" do
43
45
  json_string = @envelope.to_s
44
46
  json_data = JSON.parse(json_string)
45
47
  assert_equal 1, json_data.keys.size
46
48
  end
47
49
 
48
- it "also includes metadata, if provided" do
50
+ should "also includes metadata, if provided" do
49
51
  time = DateTime.now
50
52
  time_s = DateTime.now.to_s
51
53
  @envelope.first_posted = time
@@ -57,7 +59,7 @@ describe Timberline::Envelope do
57
59
  assert_equal time_s, json_data["first_posted"]
58
60
  end
59
61
 
60
- it "parses itself back correctly using from_json" do
62
+ should "parses itself back correctly using from_json" do
61
63
  json_string = @envelope.to_s
62
64
  new_envelope = Timberline::Envelope.from_json(json_string)
63
65
  assert_equal @envelope.contents, new_envelope.contents
@@ -1,36 +1,51 @@
1
1
  require 'test_helper'
2
2
 
3
- describe Timberline::Queue do
4
- describe "newly instantiated" do
5
- before do
3
+ class QueueTest < Test::Unit::TestCase
4
+ context "newly instantiated" do
5
+ setup do
6
6
  clear_test_db
7
7
  @queue = Timberline::Queue.new("test_queue")
8
8
  end
9
9
 
10
- it "saves the passed-in string as its queue name" do
10
+ should "saves the passed-in string as its queue name" do
11
11
  assert_equal "test_queue", @queue.queue_name
12
12
  end
13
13
 
14
- it "has a length of 0" do
14
+ should "saves its existence in timberline_queue_names" do
15
+ assert_equal true, Timberline.redis.sismember("timberline_queue_names", "test_queue")
16
+ end
17
+
18
+ should "has a length of 0" do
15
19
  assert_equal 0, @queue.length
16
20
  end
17
21
 
18
- it "has a default read_timeout of 0" do
22
+ should "starts in an unpaused state" do
23
+ assert_equal false, @queue.paused?
24
+ end
25
+
26
+ should "allows itself to be paused and unpaused" do
27
+ @queue.pause
28
+ assert_equal true, @queue.paused?
29
+ @queue.unpause
30
+ assert_equal false, @queue.paused?
31
+ end
32
+
33
+ should "has a default read_timeout of 0" do
19
34
  assert_equal 0, @queue.read_timeout
20
35
  end
21
36
 
22
- it "responds nil to a pop request after the read_timeout occurs" do
37
+ should "responds nil to a pop request after the read_timeout occurs" do
23
38
  # Let's set the read_timeout to 1 in order for this test to return
24
39
  @queue.instance_variable_set("@read_timeout", 1)
25
40
  assert_equal nil, @queue.pop
26
41
  end
27
42
 
28
- it "puts an item on the queue when that item is pushed" do
43
+ should "puts an item on the queue when that item is pushed" do
29
44
  test_item = "Test Queue Item"
30
45
  assert_equal 1, @queue.push(test_item)
31
46
  end
32
47
 
33
- it "wraps an item in an envelope when that item is pushed" do
48
+ should "wraps an item in an envelope when that item is pushed" do
34
49
  test_item = "Test Queue Item"
35
50
  assert_equal 1, @queue.push(test_item)
36
51
  data = @queue.pop
@@ -38,7 +53,7 @@ describe Timberline::Queue do
38
53
  assert_equal test_item, data.contents
39
54
  end
40
55
 
41
- it "doesn't wrap an envelope that gets pushed in another envelope" do
56
+ should "doesn't wrap an envelope that gets pushed in another envelope" do
42
57
  test_item = "Test Queue Item"
43
58
  env = Timberline::Envelope.new
44
59
  env.contents = test_item
@@ -47,25 +62,49 @@ describe Timberline::Queue do
47
62
  assert_kind_of Timberline::Envelope, data
48
63
  assert_equal test_item, data.contents
49
64
  end
65
+
66
+ should "removes everything associated with the queue when delete! is called" do
67
+ test_item = "Test Queue Item"
68
+ assert_equal 1, @queue.push(test_item)
69
+ Timberline.redis[@queue.attr("test")] = "test"
70
+ Timberline.redis[@queue.attr("foo")] = "foo"
71
+ Timberline.redis[@queue.attr("bar")] = "bar"
72
+
73
+ @queue.delete!
74
+ assert_equal nil, Timberline.redis[@queue.attr("test")]
75
+ assert_equal nil, Timberline.redis[@queue.attr("test")]
76
+ assert_equal nil, Timberline.redis[@queue.attr("test")]
77
+ assert_equal nil, Timberline.redis[@queue.queue_name]
78
+ assert_equal false, Timberline.redis.sismember("timberline_queue_names","test_queue")
79
+ end
80
+
81
+ should "uses any passed-in metadata to push when building the envelope" do
82
+ @queue.push("Howdy kids.", { :special_notes => "Super-awesome."})
83
+ assert_equal 1, @queue.length
84
+ data = @queue.pop
85
+ assert_equal "Howdy kids.", data.contents
86
+ assert_equal "Super-awesome.", data.special_notes
87
+ end
88
+
50
89
  end
51
90
 
52
- describe "with one item" do
53
- before do
91
+ context "with one item" do
92
+ setup do
54
93
  clear_test_db
55
94
  @test_item = "Test Queue Item"
56
95
  @queue = Timberline::Queue.new("test_queue")
57
96
  @queue.push(@test_item)
58
97
  end
59
98
 
60
- it "has a length of 1" do
99
+ should "has a length of 1" do
61
100
  assert_equal 1, @queue.length
62
101
  end
63
102
 
64
- it "responds to pop with the one item" do
103
+ should "responds to pop with the one item" do
65
104
  assert_equal @test_item, @queue.pop.contents
66
105
  end
67
106
 
68
- it "responds nil to a second pop" do
107
+ should "responds nil to a second pop" do
69
108
  # Let's set the read_timeout to 1 in order for this test to return
70
109
  @queue.instance_variable_set("@read_timeout", 1)
71
110
  assert_equal @test_item, @queue.pop.contents
@@ -1,65 +1,143 @@
1
1
  require 'test_helper'
2
2
 
3
- describe Timberline do
4
- before do
5
- # Reset the singleton
6
- Timberline.redis = nil
7
- Timberline.instance_variable_set("@config", nil)
8
- end
3
+ class TimberlineTest < Test::Unit::TestCase
4
+ context "Freshly set up" do
5
+ setup do
6
+ reset_timberline
7
+ end
9
8
 
10
- it "saves a passed-in redis namespace" do
11
- redis = Redis.new
12
- redisns = Redis::Namespace.new("timberline", redis)
13
- Timberline.redis = redisns
14
- assert_equal redisns, Timberline.redis
15
- end
9
+ should "creates a new queue when asked if it doesn't exist" do
10
+ queue = Timberline.queue("test_queue")
11
+ assert_kind_of Timberline::Queue, queue
12
+ assert_equal queue, Timberline.instance_variable_get("@queue_list")["test_queue"]
13
+ end
16
14
 
17
- it "Converts a standard redis server into a namespace" do
18
- redis = Redis.new
19
- Timberline.redis = redis
20
- assert_equal Redis::Namespace, Timberline.redis.class
21
- assert_equal redis, Timberline.redis.instance_variable_get("@redis")
22
- end
15
+ should "doesn't create a new queue if a queue by the same name already exists" do
16
+ queue = Timberline.queue("test_queue")
17
+ new_queue = Timberline.queue("test_queue")
18
+ assert_equal queue, new_queue
19
+ end
23
20
 
24
- it "generates a redis namespace on request if one isn't present" do
25
- assert_equal Redis::Namespace, Timberline.redis.class
26
- end
21
+ should "creates a new queue as necessary when 'push' is called and pushes the item" do
22
+ Timberline.push("test_queue", "Howdy kids.")
23
+ queue = Timberline.queue("test_queue")
24
+ assert_equal 1, queue.length
25
+ assert_equal "Howdy kids.", queue.pop.contents
26
+ end
27
27
 
28
- it "uses a default namespace of 'timberline'" do
29
- assert_equal "timberline", Timberline.redis.namespace
30
- end
28
+ should "logs the existence of the queue so that other managers can see it" do
29
+ queue = Timberline.queue("test_queue")
30
+ assert_equal 1, Timberline.all_queues.size
31
+ assert_equal "test_queue", Timberline.all_queues.first.queue_name
32
+ end
31
33
 
32
- it "can be configured" do
33
- Timberline.config do |c|
34
- c.database = 3
34
+ should "saves a passed-in redis namespace" do
35
+ redis = Redis.new
36
+ redisns = Redis::Namespace.new("timberline", redis)
37
+ Timberline.redis = redisns
38
+ assert_equal redisns, Timberline.redis
35
39
  end
36
40
 
37
- assert_equal 3, Timberline.instance_variable_get("@config").database
38
- end
41
+ should "Converts a standard redis server into a namespace" do
42
+ redis = Redis.new
43
+ Timberline.redis = redis
44
+ assert_equal Redis::Namespace, Timberline.redis.class
45
+ assert_equal redis, Timberline.redis.instance_variable_get("@redis")
46
+ end
39
47
 
40
- it "uses a pre-defined namespace name if configured" do
41
- Timberline.config do |c|
42
- c.namespace = "skyline"
48
+ should "generates a redis namespace on request if one isn't present" do
49
+ assert_equal Redis::Namespace, Timberline.redis.class
43
50
  end
44
51
 
45
- assert_equal "skyline", Timberline.redis.namespace
46
- end
52
+ should "uses a default namespace of 'timberline'" do
53
+ assert_equal "timberline", Timberline.redis.namespace
54
+ end
55
+
56
+ should "can be configured" do
57
+ Timberline.config do |c|
58
+ c.database = 3
59
+ end
60
+
61
+ assert_equal 3, Timberline.instance_variable_get("@config").database
62
+ end
47
63
 
48
- it "properly configures Redis" do
49
- @logger = Logger.new STDERR
50
- Timberline.config do |c|
51
- c.host = "localhost"
52
- c.timeout = 10
53
- c.database = 3
54
- c.logger = @logger
64
+ should "uses a pre-defined namespace name if configured" do
65
+ Timberline.config do |c|
66
+ c.namespace = "skyline"
67
+ end
68
+
69
+ assert_equal "skyline", Timberline.redis.namespace
55
70
  end
56
71
 
57
- redis = Timberline.redis
72
+ should "properly configures Redis" do
73
+ @logger = Logger.new STDERR
74
+ Timberline.config do |c|
75
+ c.host = "localhost"
76
+ c.timeout = 10
77
+ c.database = 3
78
+ c.logger = @logger
79
+ end
80
+
81
+ redis = Timberline.redis
82
+
83
+ assert_equal "localhost", redis.client.host
84
+ assert_equal 10, redis.client.timeout
85
+ assert_equal 3, redis.client.db
86
+ assert_equal @logger, redis.client.logger
87
+
88
+ end
89
+
90
+ should "allows you to retry a job that has failed" do
91
+ Timberline.push("test_queue", "Howdy kids.")
92
+ queue = Timberline.queue("test_queue")
93
+ data = queue.pop
94
+
95
+ assert_equal 0, queue.length
96
+
97
+ assert_raises Timberline::ItemRetried do
98
+ Timberline.retry_item(data)
99
+ end
100
+
101
+ assert_equal 1, queue.length
58
102
 
59
- assert_equal "localhost", redis.client.host
60
- assert_equal 10, redis.client.timeout
61
- assert_equal 3, redis.client.db
62
- assert_equal @logger, redis.client.logger
103
+ data = queue.pop
104
+ assert_equal 1, data.retries
105
+ assert_kind_of Time, Time.at(data.last_tried_at)
106
+ end
107
+
108
+ should "will continue retrying until we pass the max retries (defaults to 5)" do
109
+ Timberline.push("test_queue", "Howdy kids.")
110
+ queue = Timberline.queue("test_queue")
111
+ data = queue.pop
112
+
113
+ 5.times do |i|
114
+ assert_raises Timberline::ItemRetried do
115
+ Timberline.retry_item(data)
116
+ end
117
+ assert_equal 1, queue.length
118
+ data = queue.pop
119
+ assert_equal i + 1, data.retries
120
+ end
121
+
122
+ assert_raises Timberline::ItemErrored do
123
+ Timberline.retry_item(data)
124
+ end
125
+ assert_equal 0, queue.length
126
+ assert_equal 1, Timberline.error_queue.length
127
+ end
128
+
129
+ should "will allow you to directly error out a job" do
130
+ Timberline.push("test_queue", "Howdy kids.")
131
+ queue = Timberline.queue("test_queue")
132
+ data = queue.pop
133
+
134
+ assert_raises Timberline::ItemErrored do
135
+ Timberline.error_item(data)
136
+ end
137
+ assert_equal 1, Timberline.error_queue.length
138
+ data = Timberline.error_queue.pop
139
+ assert_kind_of Time, Time.at(data.fatal_error_at)
140
+ end
63
141
 
64
142
  end
65
143
  end
@@ -23,6 +23,9 @@ Gem::Specification.new do |s|
23
23
  # s.add_runtime_dependency "rest-client"
24
24
  s.add_runtime_dependency "redis"
25
25
  s.add_runtime_dependency "redis-namespace"
26
+ s.add_runtime_dependency "redis-expiring-set"
26
27
  s.add_runtime_dependency "trollop"
27
28
  s.add_runtime_dependency "daemons"
29
+
30
+ s.add_development_dependency "shoulda"
28
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timberline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-09 00:00:00.000000000 Z
12
+ date: 2012-02-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
- requirement: &70136237495580 !ruby/object:Gem::Requirement
16
+ requirement: &70354056089500 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70136237495580
24
+ version_requirements: *70354056089500
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis-namespace
27
- requirement: &70136237494860 !ruby/object:Gem::Requirement
27
+ requirement: &70354056077200 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,21 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70136237494860
35
+ version_requirements: *70354056077200
36
+ - !ruby/object:Gem::Dependency
37
+ name: redis-expiring-set
38
+ requirement: &70354056075820 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70354056075820
36
47
  - !ruby/object:Gem::Dependency
37
48
  name: trollop
38
- requirement: &70136237493980 !ruby/object:Gem::Requirement
49
+ requirement: &70354056073880 !ruby/object:Gem::Requirement
39
50
  none: false
40
51
  requirements:
41
52
  - - ! '>='
@@ -43,10 +54,10 @@ dependencies:
43
54
  version: '0'
44
55
  type: :runtime
45
56
  prerelease: false
46
- version_requirements: *70136237493980
57
+ version_requirements: *70354056073880
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: daemons
49
- requirement: &70136237492680 !ruby/object:Gem::Requirement
60
+ requirement: &70354056071600 !ruby/object:Gem::Requirement
50
61
  none: false
51
62
  requirements:
52
63
  - - ! '>='
@@ -54,7 +65,18 @@ dependencies:
54
65
  version: '0'
55
66
  type: :runtime
56
67
  prerelease: false
57
- version_requirements: *70136237492680
68
+ version_requirements: *70354056071600
69
+ - !ruby/object:Gem::Dependency
70
+ name: shoulda
71
+ requirement: &70354056070520 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70354056070520
58
80
  description: Timberline is a simple and extensible queuing system built in Ruby and
59
81
  backed by Redis. It makes as few assumptions as possible about how you want to interact
60
82
  with your queues while also allowing for some functionality that should be universally
@@ -75,7 +97,6 @@ files:
75
97
  - lib/timberline/config.rb
76
98
  - lib/timberline/envelope.rb
77
99
  - lib/timberline/queue.rb
78
- - lib/timberline/queue_manager.rb
79
100
  - lib/timberline/version.rb
80
101
  - test/fake_rails/config/timberline.yaml
81
102
  - test/partial_minispec.rb
@@ -84,7 +105,6 @@ files:
84
105
  - test/unit/test_config.rb
85
106
  - test/unit/test_envelope.rb
86
107
  - test/unit/test_queue.rb
87
- - test/unit/test_queue_manager.rb
88
108
  - test/unit/test_timberline.rb
89
109
  - timberline.gemspec
90
110
  homepage: http://github.com/duwanis/timberline
@@ -120,5 +140,4 @@ test_files:
120
140
  - test/unit/test_config.rb
121
141
  - test/unit/test_envelope.rb
122
142
  - test/unit/test_queue.rb
123
- - test/unit/test_queue_manager.rb
124
143
  - test/unit/test_timberline.rb
@@ -1,84 +0,0 @@
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
@@ -1,154 +0,0 @@
1
- require 'test_helper'
2
-
3
- describe Timberline::QueueManager do
4
- describe "newly instantiated" do
5
- before do
6
- clear_test_db
7
- @qm = Timberline::QueueManager.new
8
- end
9
-
10
- it "has an empty queue list" do
11
- assert_equal 0, @qm.queue_list.size
12
- end
13
-
14
- it "instantiates an error_queue" do
15
- assert_kind_of Timberline::Queue, @qm.error_queue
16
- assert_equal "timberline_errors", @qm.error_queue.queue_name
17
- end
18
-
19
- it "creates a new queue when asked if it doesn't exist" do
20
- queue = @qm.queue("test_queue")
21
- assert_kind_of Timberline::Queue, queue
22
- assert_equal 1, @qm.queue_list.size
23
- assert_equal queue, @qm.queue_list["test_queue"]
24
- end
25
-
26
- it "doesn't create a new queue if a queue by the same name already exists" do
27
- queue = @qm.queue("test_queue")
28
- new_queue = @qm.queue("test_queue")
29
- assert_equal queue, new_queue
30
- assert_equal 1, @qm.queue_list.size
31
- end
32
-
33
- it "creates a new queue as necessary when 'push' is called and pushes the item" do
34
- @qm.push("test_queue", "Howdy kids.")
35
- assert_equal 1, @qm.queue_list.size
36
- queue = @qm.queue("test_queue")
37
- assert_equal 1, queue.length
38
- assert_equal "Howdy kids.", queue.pop.contents
39
- end
40
-
41
- it "uses any passed-in metadata to push when building the envelope" do
42
- @qm.push("test_queue", "Howdy kids.", { :special_notes => "Super-awesome."})
43
- queue = @qm.queue("test_queue")
44
- assert_equal 1, queue.length
45
- data = queue.pop
46
- assert_equal "Howdy kids.", data.contents
47
- assert_equal "Super-awesome.", data.special_notes
48
- end
49
-
50
- it "includes some default information in the metadata" do
51
- @qm.push("test_queue", "Howdy kids.")
52
- queue = @qm.queue("test_queue")
53
- data = queue.pop
54
- assert_equal "Howdy kids.", data.contents
55
- assert_kind_of DateTime, DateTime.parse(data.submitted_at)
56
- assert_equal "test_queue", data.origin_queue
57
- assert_equal 0, data.retries
58
- assert_equal 1, data.job_id
59
- end
60
-
61
- it "allows you to retry a job that has failed" do
62
- @qm.push("test_queue", "Howdy kids.")
63
- queue = @qm.queue("test_queue")
64
- data = queue.pop
65
-
66
- assert_equal 0, queue.length
67
-
68
- @qm.retry_job(data)
69
-
70
- assert_equal 1, queue.length
71
-
72
- data = queue.pop
73
- assert_equal 1, data.retries
74
- assert_kind_of DateTime, DateTime.parse(data.last_tried_at)
75
- end
76
-
77
- it "will continue retrying until we pass the max retries (defaults to 5)" do
78
- @qm.push("test_queue", "Howdy kids.")
79
- queue = @qm.queue("test_queue")
80
- data = queue.pop
81
-
82
- 5.times do |i|
83
- @qm.retry_job(data)
84
- assert_equal 1, queue.length
85
- data = queue.pop
86
- assert_equal i + 1, data.retries
87
- end
88
-
89
- @qm.retry_job(data)
90
- assert_equal 0, queue.length
91
- assert_equal 1, @qm.error_queue.length
92
- end
93
-
94
- it "will allow you to directly error out a job" do
95
- @qm.push("test_queue", "Howdy kids.")
96
- queue = @qm.queue("test_queue")
97
- data = queue.pop
98
-
99
- @qm.error_job(data)
100
- assert_equal 1, @qm.error_queue.length
101
- data = @qm.error_queue.pop
102
- assert_kind_of DateTime, DateTime.parse(data.fatal_error_at)
103
- end
104
-
105
- it "will allow you to watch a queue" do
106
- @qm.push("test_queue", "Howdy kids.")
107
-
108
- assert_raises RuntimeError do
109
- @qm.watch "test_queue" do |job|
110
- if "Howdy kids." == job.contents
111
- raise "This works."
112
- end
113
- end
114
- end
115
-
116
- assert_equal 0, @qm.queue("test_queue").length
117
- end
118
-
119
- it "will allow you to retry from a queue watcher" do
120
- @qm.push("test_queue", "Howdy kids.")
121
-
122
- assert_raises RuntimeError do
123
- @qm.watch "test_queue" do |job|
124
- if "Howdy kids." == job.contents
125
- retry_job job
126
- raise "Job retried."
127
- end
128
- end
129
- end
130
-
131
- assert_equal 1, @qm.queue("test_queue").length
132
- data = @qm.queue("test_queue").pop
133
- assert_equal 1, data.retries
134
- end
135
-
136
- it "will allow you to error from a queue watcher" do
137
- @qm.push("test_queue", "Howdy kids.")
138
-
139
- assert_raises RuntimeError do
140
- @qm.watch "test_queue" do |job|
141
- if "Howdy kids." == job.contents
142
- error_job job
143
- raise "Job errored."
144
- end
145
- end
146
- end
147
-
148
- assert_equal 1, @qm.error_queue.length
149
- data = @qm.error_queue.pop
150
- assert_kind_of DateTime, DateTime.parse(data.fatal_error_at)
151
- end
152
- end
153
-
154
- end