zulu 0.0.3 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1bf65dca8019b224cf68263c2eb02030c6c73ee0
4
- data.tar.gz: 129978dfebda32e679a14923de6d8e2629339397
3
+ metadata.gz: 8ec4fc2ba4cf0c2764d457ed2063945de083073c
4
+ data.tar.gz: 911a784bda238a5d20280795b9002c8980969107
5
5
  SHA512:
6
- metadata.gz: 26cf2c9f96eaffba2a7e895d9a261e1435e975df1ddad5333101bb4b5d4636fb756c78c893af12ebf6e71a09b3984d52272a9d79b6d064832014e2af274bf546
7
- data.tar.gz: 473a95bb5c9a92a69879b159e501dcefa2efc05f408aaf380b3e51902be39705fda8988f58abcadfc7889e10048c04edf0d1de58ccb0fc4f44cfcc4114f17711
6
+ metadata.gz: 6e082fe6f5c99731a436c02dddf7cd7794438cadcb154917fa91038d5acb0523dd0a82f9e1293391a6576d45108bb8600b9e95f1f7fdb98a2ad2202af3efb8b8
7
+ data.tar.gz: 0a07fa35295615de3ab65be016ef3b08937213e1fc04e0cbbd4e0426b1d1c1cd543b7a5a02065185e7da889b02d251f61ae4452ebc698c1eb6a1f63e4cf43706
@@ -1,10 +1,10 @@
1
1
  module Zulu
2
2
  class Challenge < String
3
3
 
4
- CHARS = [(0..9),('a'..'z'),('A'..'Z')].inject([]) {|a,r| a +=r.to_a; a}
4
+ CHARS = [('0'..'9'),('a'..'z'),('A'..'Z')].inject([]) {|a,r| a +=r.to_a; a}
5
5
 
6
6
  def initialize(len=24)
7
- self << CHARS[(rand(CHARS.size - 1))] until length == len
7
+ self << CHARS.sample until length == len
8
8
  end
9
9
  end
10
10
  end
data/lib/zulu/http.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "celluloid/io"
2
+ require "net/http"
3
+ require "addressable/uri"
4
+
5
+ module Zulu
6
+ module Http
7
+
8
+ def self.post(url, options={})
9
+ uri = Addressable::URI.parse(url)
10
+ params = options.delete(:params) || {}
11
+ Net::HTTP.post_form(uri, params)
12
+ end
13
+
14
+ def self.get(url, options={})
15
+ uri = Addressable::URI.parse(url)
16
+ params = options.delete(:params)
17
+ uri.query = URI.encode_www_form(params) if params
18
+ Net::HTTP.get_response(uri)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ module Zulu
2
+ class Keeper
3
+ include Celluloid::IO
4
+ include Celluloid::Logger
5
+
6
+ def tick(now=Time.now)
7
+ debug "Looking for topics at: #{now} (#{now.to_i})"
8
+ count = 0
9
+ Topic.happening(now).each do |topic_id|
10
+ topic = Topic.new(id: topic_id)
11
+ debug "Found topic: #{topic_id}"
12
+ distribution = TopicDistribution.new(id: topic_id, now: now)
13
+ distribution.save
14
+ topic.reset_next(now)
15
+ count += 1
16
+ end
17
+ debug "Finished tick: (#{Time.now - now} seconds for #{count} topics)"
18
+ end
19
+
20
+ def start
21
+ every(Zulu.options[:interval]) { tick }
22
+ end
23
+ end
24
+ end
@@ -11,6 +11,10 @@ module Zulu
11
11
  @callback = options[:callback]
12
12
  end
13
13
 
14
+ def ==(other)
15
+ id == other.id
16
+ end
17
+
14
18
  def id
15
19
  @id ||= Digest::MD5.hexdigest [@topic, @callback].join(':')
16
20
  end
@@ -19,12 +23,19 @@ module Zulu
19
23
  @callback ||= Zulu.redis.get "#{KEY_PREFIX}:#{id}:callback"
20
24
  end
21
25
 
26
+ def topic
27
+ @topic_object ||= Topic.new(topic: @topic)
28
+ end
29
+
22
30
  def save
23
31
  Zulu.redis.set "#{KEY_PREFIX}:#{id}:callback", callback
32
+ topic.save
33
+ topic.subscribe(id)
24
34
  end
25
35
 
26
36
  def destroy
27
37
  Zulu.redis.del "#{KEY_PREFIX}:#{id}:callback"
38
+ topic.unsubscribe(id)
28
39
  end
29
40
 
30
41
  end
@@ -62,9 +62,7 @@ module Zulu
62
62
  challenge = Challenge.new
63
63
  params = to_hash
64
64
  params['hub.challenge'] = challenge
65
- uri = Addressable::URI.parse(@callback)
66
- uri.query = URI.encode_www_form(params)
67
- response = Net::HTTP.get_response(uri)
65
+ response = Http.get(@callback, params: params)
68
66
  response.code == "200" && response.body == challenge
69
67
  end
70
68
 
data/lib/zulu/topic.rb ADDED
@@ -0,0 +1,74 @@
1
+ require "digest/md5"
2
+ require "rufus/sc/cronline"
3
+
4
+ module Zulu
5
+ class Topic
6
+
7
+ KEY_PREFIX = "topic".freeze
8
+ UPCOMING_KEY = "#{KEY_PREFIX}:upcoming".freeze
9
+
10
+ def self.happening(now=Time.now)
11
+ Zulu.redis.zrangebyscore(UPCOMING_KEY, 0, now.to_i)
12
+ end
13
+
14
+ def initialize(options={})
15
+ @id = options[:id]
16
+ @topic = options[:topic]
17
+ end
18
+
19
+ def ==(other)
20
+ id == other.id
21
+ end
22
+
23
+ def id
24
+ @id ||= Digest::MD5.hexdigest @topic
25
+ end
26
+
27
+ def topic
28
+ @topic ||= Zulu.redis.get "#{KEY_PREFIX}:#{id}"
29
+ end
30
+
31
+ def subscriptions_count
32
+ Zulu.redis.scard("#{KEY_PREFIX}:#{id}:subscriptions")
33
+ end
34
+
35
+ def subscriptions
36
+ Zulu.redis.smembers("#{KEY_PREFIX}:#{id}:subscriptions")
37
+ end
38
+
39
+ def subscribe(subscription_id)
40
+ Zulu.redis.sadd("#{KEY_PREFIX}:#{id}:subscriptions", subscription_id)
41
+ end
42
+
43
+ def unsubscribe(subscription_id)
44
+ Zulu.redis.srem("#{KEY_PREFIX}:#{id}:subscriptions", subscription_id)
45
+ destroy if subscriptions_count == 0
46
+ end
47
+
48
+ def parser
49
+ @parser ||= Rufus::CronLine.new(topic)
50
+ end
51
+
52
+ def next_time(now=Time.now)
53
+ parser.next_time(now)
54
+ end
55
+
56
+ def reset_next(now=Time.now)
57
+ Zulu.redis.zadd(UPCOMING_KEY, next_time(now).to_i, id)
58
+ end
59
+
60
+ def save
61
+ Zulu.redis.multi do
62
+ Zulu.redis.set "#{KEY_PREFIX}:#{id}", topic
63
+ reset_next
64
+ end
65
+ end
66
+
67
+ def destroy
68
+ Zulu.redis.multi do
69
+ Zulu.redis.del "#{KEY_PREFIX}:#{id}"
70
+ Zulu.redis.zrem(UPCOMING_KEY, id)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ module Zulu
2
+ class TopicDistribution
3
+ LIST = "topic_distributions".freeze
4
+
5
+ def self.push(distribution)
6
+ Zulu.redis.lpush(LIST, distribution.to_json)
7
+ end
8
+
9
+ def self.pop(timeout=nil)
10
+ _, options = Zulu.redis.brpop(LIST, timeout: timeout)
11
+ options and new(Oj.load(options))
12
+ end
13
+
14
+ def initialize(options={})
15
+ @id = options[:id]
16
+ @now = options[:now]
17
+ end
18
+
19
+ def now
20
+ @now
21
+ end
22
+
23
+ def topic
24
+ @topic ||= Topic.new(id: @id)
25
+ end
26
+
27
+ def subscriptions
28
+ topic.subscriptions.map {|sid| Subscription.new(id: sid) }
29
+ end
30
+
31
+ def ==(other)
32
+ to_hash == other.to_hash
33
+ end
34
+
35
+ def to_hash
36
+ {id: @id, now: @now}
37
+ end
38
+
39
+ def to_json
40
+ Oj.dump(to_hash)
41
+ end
42
+
43
+ def save
44
+ self.class.push(self)
45
+ end
46
+
47
+ def process
48
+ subscriptions.each do |subscription|
49
+ Http.post(subscription.callback, form: {now: now})
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ require "celluloid/io"
2
+
3
+ module Zulu
4
+ class TopicDistributionProcessor
5
+ include Celluloid::IO
6
+ include Celluloid::Logger
7
+
8
+ finalizer :shutdown
9
+
10
+ def initialize
11
+ debug "Distribution Processor starting up"
12
+ end
13
+
14
+ def process
15
+ debug "Looking for a topic distribution"
16
+ distribution = TopicDistribution.pop(1)
17
+ if distribution
18
+ debug "Distribution found. Processing..."
19
+ distribution.process
20
+ end
21
+ async.reprocess
22
+ end
23
+
24
+ def reprocess
25
+ debug "Distribution Reprocessing..."
26
+ after(0) { process }
27
+ end
28
+
29
+ def shutdown
30
+ debug "Distribution Processor shutting down"
31
+ end
32
+ end
33
+ end
data/lib/zulu/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Zulu
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/zulu.rb CHANGED
@@ -7,11 +7,15 @@ require "logger"
7
7
 
8
8
  require "zulu/version"
9
9
  require "zulu/challenge"
10
+ require "zulu/http"
11
+ require "zulu/keeper"
10
12
  require "zulu/server"
11
13
  require "zulu/subscription"
12
14
  require "zulu/subscription_request"
13
15
  require "zulu/subscription_request_processor"
14
-
16
+ require "zulu/topic"
17
+ require "zulu/topic_distribution"
18
+ require "zulu/topic_distribution_processor"
15
19
 
16
20
  module Zulu
17
21
  DEFAULTS = {
@@ -20,7 +24,8 @@ module Zulu
20
24
  servers: 0,
21
25
  workers: 5,
22
26
  database: "redis://127.0.0.1:6379",
23
- keeper: false
27
+ keeper: false,
28
+ interval: 5
24
29
  }.freeze
25
30
 
26
31
  def self.options
@@ -44,17 +49,20 @@ module Zulu
44
49
  on :w, :workers, 'Run WORKERS background workers (default: 5)', argument: true, as: Integer
45
50
  on :d, :database, "Connect to DATABASE (default: redis://127.0.0.1:6379)", argument: true
46
51
  on :k, :keeper, "Run a keeper worker (default: false)"
52
+ on :i, :interval, 'Check time every INTERVAL seconds (default: 5)', argument: true, as: Integer
47
53
  end
48
54
  opts.each do |option|
49
55
  next unless option.value
50
- option.value.is_a?(String) and option.value.strip!
51
- options[option.key.to_sym] = option.value
56
+ value = option.value
57
+ value = value.strip if value.is_a?(String)
58
+ options[option.key.to_sym] = value
52
59
  end
53
60
  rescue Slop::Error => e
54
61
  abort "ERROR: #{e.message}"
55
62
  end
56
63
 
57
64
  def self.run
65
+ run_keeper if options[:keeper]
58
66
  run_workers if options[:workers] > 0
59
67
  run_servers if options[:servers] > 0
60
68
  sleep
@@ -63,11 +71,21 @@ module Zulu
63
71
  end
64
72
 
65
73
  def self.stop
74
+ stop_keeper if options[:keeper]
66
75
  stop_servers if options[:servers] > 0
67
76
  stop_workers if options[:workers] > 0
68
77
  exit
69
78
  end
70
79
 
80
+ def self.run_keeper
81
+ Keeper.supervise_as :keeper
82
+ Celluloid::Actor[:keeper].async.start
83
+ end
84
+
85
+ def self.stop_keeper
86
+ Celluloid::Actor[:keeper].terminate if Celluloid::Actor[:worker_supervisor]
87
+ end
88
+
71
89
  def self.run_servers
72
90
  Rack::Handler::Reel.run Zulu::Server, port: options[:port],
73
91
  host: options[:host],
@@ -84,7 +102,11 @@ module Zulu
84
102
  Celluloid::Actor[:worker_supervisor].pool(SubscriptionRequestProcessor,
85
103
  as: :request_processors,
86
104
  size: options[:workers])
105
+ Celluloid::Actor[:worker_supervisor].pool(TopicDistributionProcessor,
106
+ as: :distribution_processors,
107
+ size: options[:workers])
87
108
  Celluloid::Actor[:request_processors].async.process
109
+ Celluloid::Actor[:distribution_processors].async.process
88
110
  end
89
111
 
90
112
  def self.stop_workers
@@ -5,7 +5,6 @@ require 'minitest/autorun'
5
5
  require 'minitest/pride'
6
6
  require 'minitest/english/deny'
7
7
  require 'rack/test'
8
- require "webmock/minitest"
9
8
 
10
9
  require 'celluloid/autostart'
11
10
  require 'zulu'
@@ -12,7 +12,7 @@ class TestChallenge < MiniTest::Test
12
12
  end
13
13
 
14
14
  def test_it_produces_appropriate_string
15
- assert_match /[a-zA-Z0-9]/, Challenge.new
15
+ assert_match /^[a-zA-Z0-9]+$/, Challenge.new
16
16
  end
17
17
 
18
18
  end
data/test/test_http.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestHttp < MiniTest::Test
4
+ include Zulu
5
+
6
+ def test_it_returns_response_for_get
7
+ response = Object.new
8
+ Net::HTTP.stub(:get_response, response) do
9
+ assert_equal response, Http.get('http://www.example.com')
10
+ end
11
+ end
12
+
13
+ def test_it_returns_response_for_post
14
+ response = Object.new
15
+ Net::HTTP.stub(:post_form, response) do
16
+ assert_equal response, Http.post('http://www.example.com')
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,32 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestKeeper < MiniTest::Test
4
+ include Zulu
5
+
6
+ def teardown
7
+ Zulu.redis.flushall
8
+ end
9
+
10
+ def topic_id
11
+ '5906891b49b573503fcdcce058f1ef5d'
12
+ end
13
+
14
+ def topic_cron
15
+ '*/15 * * * *'
16
+ end
17
+
18
+ def topic_options_without_id
19
+ {topic: topic_cron}
20
+ end
21
+
22
+ def test_it_resets_next_on_tick
23
+ topic = Topic.new(topic_options_without_id)
24
+ now = Time.new(2013,8,1)
25
+ Time.stub(:now, now) do
26
+ topic.save
27
+ end
28
+ keeper = Keeper.new
29
+ keeper.tick now + 15 * 60
30
+ assert_equal (now + 30 * 60).to_i, Zulu.redis.zscore(Topic::UPCOMING_KEY, topic_id)
31
+ end
32
+ end
@@ -34,6 +34,34 @@ class TestSubscription < MiniTest::Test
34
34
  assert_equal subscription_callback, callback
35
35
  end
36
36
 
37
+ def test_it_saves_topic_on_save
38
+ subscription = Subscription.new(subscription_options_without_id)
39
+ mock_topic = MiniTest::Mock.new
40
+ metaclass = class << mock_topic; self; end
41
+ metaclass.send(:define_method, :subscribe) do |_|
42
+ true
43
+ end
44
+ mock_topic.expect(:save, true)
45
+ subscription.stub(:topic, mock_topic) do
46
+ subscription.save
47
+ end
48
+ mock_topic.verify
49
+ end
50
+
51
+ def test_it_subscribes_to_topic_on_save
52
+ subscription = Subscription.new(subscription_options_without_id)
53
+ mock_topic = MiniTest::Mock.new
54
+ metaclass = class << mock_topic; self; end
55
+ metaclass.send(:define_method, :save) do
56
+ true
57
+ end
58
+ mock_topic.expect(:subscribe, true, [subscription_id])
59
+ subscription.stub(:topic, mock_topic) do
60
+ subscription.save
61
+ end
62
+ mock_topic.verify
63
+ end
64
+
37
65
  def test_it_retrieves_callback_for_id
38
66
  Subscription.new(subscription_options_without_id).save
39
67
  callback = Subscription.new(id: subscription_id).callback
@@ -47,4 +75,15 @@ class TestSubscription < MiniTest::Test
47
75
  assert_nil Zulu.redis.get "#{Subscription::KEY_PREFIX}:#{subscription_id}:callback"
48
76
  end
49
77
 
78
+ def test_it_unsubscribes_to_topic_on_destroy
79
+ subscription = Subscription.new(subscription_options_without_id)
80
+ subscription.save
81
+ mock_topic = MiniTest::Mock.new
82
+ mock_topic.expect(:unsubscribe, true, [subscription_id])
83
+ subscription.stub(:topic, mock_topic) do
84
+ subscription.destroy
85
+ end
86
+ mock_topic.verify
87
+ end
88
+
50
89
  end
@@ -5,7 +5,6 @@ class TestSubscriptionRequest < MiniTest::Test
5
5
 
6
6
  def teardown
7
7
  Zulu.redis.flushall
8
- WebMock.reset!
9
8
  end
10
9
 
11
10
  def subscribe_options(opts={})
@@ -132,31 +131,36 @@ class TestSubscriptionRequest < MiniTest::Test
132
131
  def test_it_requests_callback_on_verify
133
132
  challenge = 'foobar'
134
133
  request = subscribe_request
135
- stub_get = stub_http_request(:get, "www.example.org/callback").
136
- with(query: subscribe_options('hub.challenge' => challenge)).
137
- to_return(body: challenge)
138
- Challenge.stub(:new, challenge) do
139
- request.verify
134
+ response = MiniTest::Mock.new
135
+ response.expect(:code, "200")
136
+ response.expect(:body, challenge)
137
+ Http.stub(:get, response) do
138
+ Challenge.stub(:new, challenge) do
139
+ request.verify
140
+ end
140
141
  end
141
- assert_requested stub_get
142
+ response.verify
142
143
  end
143
144
 
144
145
  def test_it_passes_on_good_verify
145
146
  challenge = 'foobar'
146
147
  request = subscribe_request
147
- stub_get = stub_http_request(:get, "www.example.org/callback").
148
- with(query: subscribe_options('hub.challenge' => challenge)).
149
- to_return(body: challenge)
150
- Challenge.stub(:new, challenge) do
151
- assert request.verify
148
+ response = MiniTest::Mock.new
149
+ response.expect(:code, "200")
150
+ response.expect(:body, challenge)
151
+ Http.stub(:get, response) do
152
+ Challenge.stub(:new, challenge) do
153
+ assert request.verify
154
+ end
152
155
  end
153
156
  end
154
157
 
155
158
  def test_it_fails_on_bad_verify
156
- stub_get = stub_http_request(:get, "www.example.org/callback").
157
- with(query: hash_including(subscribe_options)).
158
- to_return(status: [404, "Not Found"])
159
- deny subscribe_request.verify
159
+ response = MiniTest::Mock.new
160
+ response.expect(:code, "404")
161
+ Http.stub(:get, response) do
162
+ deny subscribe_request.verify
163
+ end
160
164
  end
161
165
 
162
166
  def test_it_creates_subscription_on_subscribe
@@ -5,7 +5,6 @@ class TestSubscriptionRequestProcessor < MiniTest::Test
5
5
 
6
6
  def teardown
7
7
  Zulu.redis.flushall
8
- WebMock.reset!
9
8
  end
10
9
 
11
10
  def test_it_processes_request
@@ -0,0 +1,143 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestSubscription < MiniTest::Test
4
+ include Zulu
5
+
6
+ def teardown
7
+ Zulu.redis.flushall
8
+ end
9
+
10
+ def topic_id
11
+ '5906891b49b573503fcdcce058f1ef5d'
12
+ end
13
+
14
+ def subscription_id
15
+ '3a2d38ef403d8f494988a9e1513e7ce0'
16
+ end
17
+
18
+ def topic_cron
19
+ '*/15 * * * *'
20
+ end
21
+
22
+ def topic_options_without_id
23
+ {topic: topic_cron}
24
+ end
25
+
26
+ def test_it_generates_an_id_for_topic
27
+ topic = Topic.new(topic_options_without_id)
28
+ assert_equal topic_id, topic.id
29
+ end
30
+
31
+ def test_it_calculates_next_time
32
+ topic = Topic.new(topic_options_without_id)
33
+ now = Time.new(2013,8,1)
34
+ assert_equal now + 15 * 60, topic.next_time(now)
35
+ end
36
+
37
+ def test_it_stores_next_time_on_reset
38
+ topic = Topic.new(topic_options_without_id)
39
+ now = Time.new(2013,8,1)
40
+ topic.reset_next(now)
41
+ topic_next = Zulu.redis.zscore Topic::UPCOMING_KEY, topic_id
42
+ assert_equal (now + 15 * 60).to_i, topic_next
43
+ end
44
+
45
+ def test_it_stores_next_time_on_save
46
+ topic = Topic.new(topic_options_without_id)
47
+ now = Time.new(2013,8,1)
48
+ Time.stub(:now, now) do
49
+ topic.save
50
+ end
51
+ topic_next = Zulu.redis.zscore Topic::UPCOMING_KEY, topic_id
52
+ assert_equal (now + 15 * 60).to_i, topic_next
53
+ end
54
+
55
+ def test_it_stores_topic_on_save
56
+ topic = Topic.new(topic_options_without_id)
57
+ topic.save
58
+ topic_topic = Zulu.redis.get "#{Topic::KEY_PREFIX}:#{topic_id}"
59
+ assert_equal topic_cron, topic_topic
60
+ end
61
+
62
+ def test_it_retrieves_topic_for_id
63
+ Topic.new(topic_options_without_id).save
64
+ topic_topic = Topic.new(id: topic_id).topic
65
+ assert_equal topic_cron, topic_topic
66
+ end
67
+
68
+ def test_it_removes_topic_on_destroy
69
+ topic = Topic.new(topic_options_without_id)
70
+ topic.save
71
+ topic.destroy
72
+ assert_nil Zulu.redis.get "#{Topic::KEY_PREFIX}:#{topic_id}"
73
+ end
74
+
75
+ def test_it_removes_topic_from_upcoming_on_destroy
76
+ topic = Topic.new(topic_options_without_id)
77
+ topic.save
78
+ topic.destroy
79
+ assert_empty Zulu.redis.zrange Topic::UPCOMING_KEY, 0, -1
80
+ end
81
+
82
+ def test_it_finds_present_topic_in_happening
83
+ topic = Topic.new(topic_options_without_id)
84
+ now = Time.new(2013,8,1)
85
+ Time.stub(:now, now) do
86
+ topic.save
87
+ end
88
+ assert_includes Topic.happening(now + 15 * 60), topic.id
89
+ end
90
+
91
+ def test_it_finds_past_topic_in_happening
92
+ topic = Topic.new(topic_options_without_id)
93
+ now = Time.new(2013,8,1)
94
+ Time.stub(:now, now) do
95
+ topic.save
96
+ end
97
+ assert_includes Topic.happening(now + 20 * 60), topic.id
98
+ end
99
+
100
+ def test_it_does_not_find_future_topic_in_happening
101
+ topic = Topic.new(topic_options_without_id)
102
+ now = Time.new(2013,8,1)
103
+ Time.stub(:now, now) do
104
+ topic.save
105
+ end
106
+ deny_includes Topic.happening(now), topic.id
107
+ end
108
+
109
+ def test_it_adds_subscription_on_subscribe
110
+ topic = Topic.new(topic_options_without_id)
111
+ topic.subscribe subscription_id
112
+ assert_includes topic.subscriptions, subscription_id
113
+ end
114
+
115
+ def test_it_increments_subscription_count_on_subscribe
116
+ topic = Topic.new(topic_options_without_id)
117
+ topic.subscribe subscription_id
118
+ assert_equal 1, topic.subscriptions_count
119
+ end
120
+
121
+ def test_it_removes_subscription_on_unsubscribe
122
+ topic = Topic.new(topic_options_without_id)
123
+ topic.subscribe subscription_id
124
+ topic.unsubscribe subscription_id
125
+ deny_includes topic.subscriptions, subscription_id
126
+ end
127
+
128
+ def test_it_decrements_subscription_count_on_unsubscribe
129
+ topic = Topic.new(topic_options_without_id)
130
+ topic.subscribe subscription_id
131
+ topic.unsubscribe subscription_id
132
+ assert_equal 0, topic.subscriptions_count
133
+ end
134
+
135
+ def test_it_destroys_topic_from_upcoming_on_destroy
136
+ topic = Topic.new(topic_options_without_id)
137
+ topic.save
138
+ topic.subscribe subscription_id
139
+ topic.unsubscribe subscription_id
140
+ assert_nil Zulu.redis.get "#{Topic::KEY_PREFIX}:#{topic_id}"
141
+ end
142
+
143
+ end
@@ -0,0 +1,81 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestTopicDistribution < MiniTest::Test
4
+ include Zulu
5
+
6
+ def teardown
7
+ Zulu.redis.flushall
8
+ end
9
+
10
+ def topic_id
11
+ '5906891b49b573503fcdcce058f1ef5d'
12
+ end
13
+
14
+ def subscription_id
15
+ '3a2d38ef403d8f494988a9e1513e7ce0'
16
+ end
17
+
18
+ def time_now
19
+ Time.new(2013,8,1)
20
+ end
21
+
22
+ def distribution_options
23
+ {id: topic_id, now: time_now}
24
+ end
25
+
26
+ def topic_distribution
27
+ TopicDistribution.new(distribution_options)
28
+ end
29
+
30
+ def test_it_pops_the_next_distribution
31
+ distribution = topic_distribution
32
+ distribution.save
33
+ assert_equal distribution, TopicDistribution.pop
34
+ end
35
+
36
+ def test_it_accepts_timeout_for_pop
37
+ mock = MiniTest::Mock.new
38
+ mock.expect(:brpop, nil, [TopicDistribution::LIST, timeout: 5])
39
+ Zulu.stub(:redis, mock) do
40
+ TopicDistribution.pop(5)
41
+ end
42
+ mock.verify
43
+ end
44
+
45
+ def test_it_proxies_topic
46
+ assert_equal Topic.new(id: topic_id), topic_distribution.topic
47
+ end
48
+
49
+ def test_it_returns_subscriptions_for_topic
50
+ topic = Topic.new(id: topic_id)
51
+ topic.subscribe subscription_id
52
+ assert_includes topic_distribution.subscriptions,
53
+ Subscription.new(id: subscription_id)
54
+ end
55
+
56
+ def test_it_reconstructs_options
57
+ assert_equal distribution_options, topic_distribution.to_hash
58
+ end
59
+
60
+ def test_it_converts_options_to_json
61
+ assert_equal Oj.dump(distribution_options), topic_distribution.to_json
62
+ end
63
+
64
+ def test_it_adds_self_to_queue_on_save
65
+ distribution = topic_distribution
66
+ distribution.save
67
+ assert_equal distribution.to_json, Zulu.redis.rpop(TopicDistribution::LIST)
68
+ end
69
+
70
+ def test_it_posts_to_subscription_callback
71
+ distribution = topic_distribution
72
+ subscription = MiniTest::Mock.new
73
+ subscription.expect(:callback, 'www.example.com')
74
+ Http.stub(:post, true) do
75
+ distribution.stub(:subscriptions, [subscription]) do
76
+ distribution.process
77
+ end
78
+ end
79
+ subscription.verify
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestTopicDistributionProcessor < MiniTest::Test
4
+ include Zulu
5
+
6
+ def teardown
7
+ Zulu.redis.flushall
8
+ end
9
+
10
+ def test_it_processes_request
11
+ processor = TopicDistributionProcessor.new
12
+
13
+ distribution = MiniTest::Mock.new
14
+ distribution.expect(:process, true)
15
+
16
+ TopicDistribution.stub(:pop, distribution) do
17
+ processor.stub(:reprocess, true) do
18
+ processor.process
19
+ end
20
+ end
21
+
22
+ distribution.verify
23
+ end
24
+ end
data/test/test_zulu.rb CHANGED
@@ -37,6 +37,10 @@ class TestZulu < MiniTest::Test
37
37
  deny Zulu.options[:keeper]
38
38
  end
39
39
 
40
+ def test_it_has_default_option_for_interval
41
+ assert_equal 5, Zulu.options[:interval]
42
+ end
43
+
40
44
  def test_it_accepts_option_for_port
41
45
  Zulu.parse_options(['-p 3000'])
42
46
  assert_equal 3000, Zulu.options[:port]
@@ -67,6 +71,11 @@ class TestZulu < MiniTest::Test
67
71
  assert Zulu.options[:keeper]
68
72
  end
69
73
 
74
+ def test_it_accepts_option_for_interval
75
+ Zulu.parse_options(['-i 30'])
76
+ assert_equal 30, Zulu.options[:interval]
77
+ end
78
+
70
79
  def test_it_aborts_on_option_with_missing_arg
71
80
  assert_raises(SystemExit) do
72
81
  out, err = capture_io { Zulu.parse_options(['-p']) }
data/zulu.gemspec CHANGED
@@ -25,11 +25,11 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency "celluloid-redis", "~> 0.0.2"
26
26
  spec.add_dependency "oj", "~> 2.1.4"
27
27
  spec.add_dependency "addressable", "~> 2.3.5"
28
+ spec.add_dependency "rufus-scheduler", "~> 2.0.22"
28
29
 
29
30
  spec.add_development_dependency "bundler", "~> 1.3"
30
31
  spec.add_development_dependency "rake"
31
32
  spec.add_development_dependency "minitest", "~> 5.0.6"
32
33
  spec.add_development_dependency "minitest-english", "~> 0.1.0"
33
34
  spec.add_development_dependency "rack-test", "~> 0.6.2"
34
- spec.add_development_dependency "webmock", "~> 1.13.0"
35
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zulu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Weinand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-27 00:00:00.000000000 Z
11
+ date: 2013-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slop
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ~>
109
109
  - !ruby/object:Gem::Version
110
110
  version: 2.3.5
111
+ - !ruby/object:Gem::Dependency
112
+ name: rufus-scheduler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 2.0.22
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 2.0.22
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: bundler
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -178,20 +192,6 @@ dependencies:
178
192
  - - ~>
179
193
  - !ruby/object:Gem::Version
180
194
  version: 0.6.2
181
- - !ruby/object:Gem::Dependency
182
- name: webmock
183
- requirement: !ruby/object:Gem::Requirement
184
- requirements:
185
- - - ~>
186
- - !ruby/object:Gem::Version
187
- version: 1.13.0
188
- type: :development
189
- prerelease: false
190
- version_requirements: !ruby/object:Gem::Requirement
191
- requirements:
192
- - - ~>
193
- - !ruby/object:Gem::Version
194
- version: 1.13.0
195
195
  description: A standalone PuSH-inspired service for scheduling web hook execution
196
196
  email:
197
197
  - dweinand@gmail.com
@@ -209,17 +209,27 @@ files:
209
209
  - bin/zulu
210
210
  - lib/zulu.rb
211
211
  - lib/zulu/challenge.rb
212
+ - lib/zulu/http.rb
213
+ - lib/zulu/keeper.rb
212
214
  - lib/zulu/server.rb
213
215
  - lib/zulu/subscription.rb
214
216
  - lib/zulu/subscription_request.rb
215
217
  - lib/zulu/subscription_request_processor.rb
218
+ - lib/zulu/topic.rb
219
+ - lib/zulu/topic_distribution.rb
220
+ - lib/zulu/topic_distribution_processor.rb
216
221
  - lib/zulu/version.rb
217
222
  - test/minitest_helper.rb
218
223
  - test/test_challenge.rb
224
+ - test/test_http.rb
225
+ - test/test_keeper.rb
219
226
  - test/test_server.rb
220
227
  - test/test_subscription.rb
221
228
  - test/test_subscription_request.rb
222
229
  - test/test_subscription_request_processor.rb
230
+ - test/test_topic.rb
231
+ - test/test_topic_distribution.rb
232
+ - test/test_topic_distribution_processor.rb
223
233
  - test/test_zulu.rb
224
234
  - zulu.gemspec
225
235
  homepage: ''
@@ -249,8 +259,13 @@ summary: PuSH-inspired web hook scheduler
249
259
  test_files:
250
260
  - test/minitest_helper.rb
251
261
  - test/test_challenge.rb
262
+ - test/test_http.rb
263
+ - test/test_keeper.rb
252
264
  - test/test_server.rb
253
265
  - test/test_subscription.rb
254
266
  - test/test_subscription_request.rb
255
267
  - test/test_subscription_request_processor.rb
268
+ - test/test_topic.rb
269
+ - test/test_topic_distribution.rb
270
+ - test/test_topic_distribution_processor.rb
256
271
  - test/test_zulu.rb