wind_up 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 79c283c998f19e33759901fca41cfb5c00219db2
4
+ data.tar.gz: 739bf5be6f00edba35343cc8f1fbf9a15b909ad8
5
+ SHA512:
6
+ metadata.gz: 6ae64913bcdd2eebcc0bd26f12a8c0649d259b44cb3bb191a35f631c842604791d7111a36a60c62f28d336ca17ed221304f55aad8bcdf83e5085defc0be1c98e
7
+ data.tar.gz: e13d341414cb2b0ad0c65cdf7757648bbd37110dcc4e5435c782205e81d01153f9c42cab2907d32b38709574218155f138085100c0ae5b915917f4dea820a92a
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ wind_up
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0-p0
data/CHANGES.md ADDED
@@ -0,0 +1,9 @@
1
+ CHANGELOG
2
+ =========
3
+
4
+ ## 0.0.2 ##
5
+ * Use Akka-style routers
6
+
7
+
8
+ ## 0.0.1 ##
9
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wind_up.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ wind_up (0.0.2)
5
+ celluloid (~> 0.14.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ celluloid (0.14.1)
11
+ timers (>= 1.0.0)
12
+ diff-lcs (1.2.4)
13
+ rake (10.1.0)
14
+ rspec (2.13.0)
15
+ rspec-core (~> 2.13.0)
16
+ rspec-expectations (~> 2.13.0)
17
+ rspec-mocks (~> 2.13.0)
18
+ rspec-core (2.13.1)
19
+ rspec-expectations (2.13.0)
20
+ diff-lcs (>= 1.1.3, < 2.0)
21
+ rspec-mocks (2.13.1)
22
+ timers (1.1.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ rake
29
+ rspec
30
+ wind_up!
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ryan Chan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ Wind Up
2
+ =======
3
+ WindUp is a drop-in replacement for Celluloid's `PoolManager` class. So why
4
+ would you use WindUp?
5
+
6
+ * Asynchronous message passing - get all the nice worker-level concurrency
7
+ Celluloid give you (#sleep, #Celluloid::IO, #future, etc)
8
+ * Separate proxies for QueueManager and queues - no more unexpected behavior
9
+ between #is_a? and #class
10
+ * Single queue handles multiple workers - Extend WindUp with
11
+ [MultiWindUp](https://www.github.com/ryanlchan/multi_wind_up) and you can
12
+ have one pool execute multiple types of workers simultaneously
13
+
14
+ Usage
15
+ -----
16
+
17
+ WindUp `Queues` are almost drop-in replacements for Celluloid pools.
18
+
19
+ ```ruby
20
+ q = AnyCelluloidClass.queue size: 3 # size defaults to number of cores
21
+ q.any_method # perform synchronously
22
+ q.async.long_running_method # perform asynchronously
23
+ q.future.i_want_this_back # perform as a future
24
+ ```
25
+
26
+ `Queues` use two separate proxies to control `Queue` commands vs
27
+ `QueueManager` commands.
28
+ ```ruby
29
+ # .queue returns the proxy for the queue (i.e. workers)
30
+ q = AnyCelluloidClass.queue # => WindUp::QueueProxy(AnyCelluloidClass)
31
+
32
+ # Get the proxy for the manager from the QueueProxy
33
+ q.__manager__ # => Celluloid::ActorProxy(WindUp::QueueManager)
34
+
35
+ # Return to the queue from the manager
36
+ q.__manager__.queue # WindUp::QueueProxy(AnyCelluloidClass)
37
+ ```
38
+
39
+ You may store these `Queue` object in the registry as any actor
40
+ ```ruby
41
+ Celluloid::Actor[:queue] = q
42
+ ```
43
+
44
+ Changing Routing Behavior (Advanced)
45
+ ------------------------------------
46
+
47
+ WindUp accepts multiple types of routing behavior. Supported behaviors include:
48
+
49
+ * :random - Route messages to workers randomly
50
+ * :round_robin - Route messages to each worker sequentially
51
+ * :smallest_mailbox - Route messages to the worker with the smallest mailbox
52
+ * :first_available - Route messages to the first available worker (Default)
53
+
54
+ To configure your queue to use a specific routing style, specify the routing
55
+ behavior when calling .queue:
56
+
57
+ ```ruby
58
+ # Use a random router
59
+ Klass.queue router: :random
60
+ ```
61
+
62
+ You can specify your own custom routing behavior as well:
63
+ ```ruby
64
+ class FirstRouter < WindUp::Router::Base
65
+ # Returns the next subscriber to route a message to
66
+ def next_subscriber
67
+ subscribers.first
68
+ end
69
+ end
70
+
71
+ # Register this router
72
+ WindUp::Routers.register :first, FirstRouter
73
+
74
+ # Use this router
75
+ q = Klass.queue router: :first
76
+
77
+ ```
78
+
79
+ Multiple worker types per queue
80
+ -------------------------------
81
+
82
+ This part of WindUp has been extracted to its own gem,
83
+ [MultiWindUp](https://www.github.com/ryanlchan/multi_wind_up).
84
+ WindUp now only contains the pooling implementation.
85
+
86
+ ## Contributing
87
+
88
+ 1. Fork it
89
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
90
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
91
+ 4. Push to the branch (`git push origin my-new-feature`)
92
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ # If you want to make this the default task
7
+ task :default => :spec
8
+ task :test => :spec
9
+
10
+ task :console do
11
+ exec "irb -r wind_up -I ./lib"
12
+ end
13
+
14
+ task :rspec do
15
+ exec "rspec -f doc --color"
16
+ end
@@ -0,0 +1,27 @@
1
+ module WindUp
2
+ # WindUp's ForwardedCall tells an actor to pull a message from a source
3
+ # mailbox when processed
4
+ class ForwardedCall < Celluloid::Call
5
+
6
+ # Do not block if no work found
7
+ TIMEOUT = 0
8
+
9
+ def initialize(source)
10
+ @source = source
11
+ end
12
+
13
+ # Pull the next message from the source, if available
14
+ def dispatch(obj)
15
+ msg = @source.receive(TIMEOUT)
16
+ ::Celluloid.mailbox << msg if msg
17
+ end
18
+ end
19
+
20
+ # Wraps a TerminationRequest for standard ordering within a mailbox
21
+ class DelayedTerminationRequest < Celluloid::Call
22
+ def initialize
23
+ @method = :terminate
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,10 @@
1
+ # Extend Celluloid with the ability to use Klass.queue
2
+ module WindUp
3
+ module CelluloidExts
4
+ def queue(options = {})
5
+ WindUp::QueueManager.new(self, options).queue
6
+ end
7
+ end
8
+ end
9
+
10
+ Celluloid::ClassMethods.send :include, WindUp::CelluloidExts
@@ -0,0 +1,3 @@
1
+ module WindUp
2
+ class MissingWorkerClass < StandardError; end
3
+ end
@@ -0,0 +1,138 @@
1
+ # Manages a queue of workers
2
+ # Accumulates/stores messages and supervises a group of workers
3
+ # WindUp `Queues` are almost drop-in replacements for Celluloid pools.
4
+ #
5
+ # ```ruby
6
+ # q = AnyCelluloidClass.queue size: 3 # size defaults to number of cores
7
+ # q.any_method # perform synchronously
8
+ # q.async.long_running_method # perform asynchronously
9
+ # q.future.i_want_this_back # perform as a future
10
+ # ```
11
+ #
12
+ # `Queues` use two separate proxies to control `Queue` commands vs
13
+ # `QueueManager` commands.
14
+ # ```ruby
15
+ # # .queue returns the proxy for the queue (i.e. workers)
16
+ # q = AnyCelluloidClass.queue # => WindUp::QueueProxy(AnyCelluloidClass)
17
+ #
18
+ # # Get the proxy for the manager from the QueueProxy
19
+ # q.__manager__ # => Celluloid::ActorProxy(WindUp::QueueManager)
20
+ #
21
+ # # Return to the queue from the manager
22
+ # q.__manager__.queue # WindUp::QueueProxy(AnyCelluloidClass)
23
+ # ```
24
+ #
25
+ # You may store these `Queue` object in the registry as any actor
26
+ # ```ruby
27
+ # Celluloid::Actor[:queue] = q
28
+ # ```
29
+ module WindUp
30
+ class QueueManager
31
+ include Celluloid
32
+ attr_reader :size, :router, :worker_class
33
+
34
+ trap_exit :restart_actor
35
+
36
+ # Don't use QueueManager.new, use Klass.queue instead
37
+ def initialize(worker_class, options = {})
38
+ defaults = { :size => [Celluloid.cores, 2].max,
39
+ :router => :first_available }
40
+ options = defaults.merge options
41
+
42
+ @worker_class = worker_class
43
+ @args = options[:args] ? Array(options[:args]) : []
44
+ @size = options[:size]
45
+
46
+ router_class = Routers[options[:router]]
47
+ raise ArgumentError, "Router class not recognized" unless router_class
48
+ @router = router_class.new
49
+
50
+ @registry = Celluloid::Registry.root
51
+ @group = []
52
+ resize_group
53
+ end
54
+
55
+ # Terminate our supervised group on finalization
56
+ finalizer :__shutdown__
57
+ def __shutdown__
58
+ @router.shutdown
59
+ group.reverse_each(&:terminate)
60
+ end
61
+
62
+ ###########
63
+ # Helpers #
64
+ ###########
65
+
66
+ # Access the Queue's proxy
67
+ def queue
68
+ WindUp::QueueProxy.new Actor.current
69
+ end
70
+
71
+ # Resize this queue's worker group
72
+ # NOTE: Using this to down-size your queue CAN truncate ongoing work!
73
+ # Workers which are waiting on blocks/sleeping will receive a termination
74
+ # request prematurely!
75
+ # @param num [Integer] Number of workers to use
76
+ def size=(num)
77
+ @size = num
78
+ resize_group
79
+ end
80
+
81
+ # Return the size of the queue backlog
82
+ # @return [Integer] the number of messages queueing
83
+ def backlog
84
+ @router.size
85
+ end
86
+
87
+ def inspect
88
+ "<Celluloid::ActorProxy(#{self.class}) @size=#{@size} @worker_class=#{@worker_class} @backlog=#{backlog}>"
89
+ end
90
+
91
+ ####################
92
+ # Group Management #
93
+ ####################
94
+
95
+ # Restart a crashed actor
96
+ def restart_actor(actor, reason)
97
+ member = group.find do |_member|
98
+ _member.actor == actor
99
+ end
100
+ raise "A group member went missing. This shouldn't be!" unless member
101
+
102
+ if reason
103
+ member.restart(reason)
104
+ else
105
+ # Remove from group on clean shutdown
106
+ group.delete_if do |_member|
107
+ _member.actor == actor
108
+ end
109
+ end
110
+ end
111
+
112
+ private
113
+ def group
114
+ @group ||= []
115
+ end
116
+
117
+ # Resize the worker group in this queue
118
+ # You should probably be using #size=
119
+ # @param target [Integer] the targeted number of workers to grow to
120
+ def resize_group(target = size)
121
+ delta = target - group.size
122
+ if delta == 0
123
+ # *Twiddle thumbs*
124
+ return
125
+ elsif delta > 0
126
+ # Increase pool size
127
+ delta.times do
128
+ worker = Celluloid::SupervisionGroup::Member.new @registry, @worker_class, :args => @args
129
+ group << worker
130
+ @router.add_subscriber(worker.actor.mailbox)
131
+ end
132
+ else
133
+ # Truncate pool
134
+ delta.abs.times { @router << DelayedTerminationRequest.new }
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,37 @@
1
+ # A proxy object which sends calls to a Queue mailbox
2
+ module WindUp
3
+ class QueueProxy < Celluloid::ActorProxy
4
+ def initialize(manager)
5
+ @mailbox = manager.router
6
+ @klass = manager.worker_class.to_s
7
+ @sync_proxy = ::Celluloid::SyncProxy.new(@mailbox, @klass)
8
+ @async_proxy = ::Celluloid::AsyncProxy.new(@mailbox, @klass)
9
+ @future_proxy = ::Celluloid::FutureProxy.new(@mailbox, @klass)
10
+
11
+ @manager_proxy = manager
12
+ end
13
+
14
+ # Escape route to access the QueueManager actor from the QueueProxy
15
+ def __manager__
16
+ @manager_proxy
17
+ end
18
+
19
+ # Reroute termination/alive? to the queue manager
20
+ def terminate
21
+ __manager__.terminate
22
+ end
23
+
24
+ def terminate!
25
+ __manager__.terminate!
26
+ end
27
+
28
+ def alive?
29
+ __manager__.alive?
30
+ end
31
+
32
+ def inspect
33
+ orig = super
34
+ orig.sub("Celluloid::ActorProxy", "WindUp::QueueProxy")
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ module WindUp
2
+ class Railtie < ::Rails::Railtie
3
+ initializer "wind_up.logger" do
4
+ WindUp.logger = Rails.logger
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,132 @@
1
+ # A duck-type of Celluloid::Mailbox which forwards messages to subscribers
2
+ #
3
+ # Router is not a mailbox in the strict sense. It acts as a forwarder for
4
+ # calls, accepting and routing them to the next available subscriber. As with
5
+ # Akka routers, pushing a call onto a Router is invoked concurrently from the
6
+ # sender and must be threadsafe.
7
+ module WindUp
8
+ module Routers
9
+ def self.register(name, klass)
10
+ registry[name] = klass
11
+ end
12
+
13
+ def self.registry
14
+ @registry ||= {}
15
+ end
16
+
17
+ def self.[](name)
18
+ registry[name] || registry.values.first
19
+ end
20
+ end
21
+
22
+ module Router
23
+ class Base
24
+ def initialize
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ # List all subscribers
29
+ def subscribers
30
+ @subscribers ||= []
31
+ end
32
+
33
+ # Subscribe to this mailbox for updates of new messages
34
+ # @param subscriber [Object] the subscriber to send messages to
35
+ def add_subscriber(subscriber)
36
+ @mutex.synchronize do
37
+ subscribers << subscriber unless subscribers.include?(subscriber)
38
+ end
39
+ end
40
+
41
+ # Remove a subscriber from thie mailbox
42
+ # @param subscriber [Object] the subscribed object
43
+ def remove_subscriber(subscriber)
44
+ @mutex.synchronize do
45
+ subscribers.delete subscriber
46
+ end
47
+ end
48
+
49
+ # Send the call to all subscribers
50
+ def <<(message)
51
+ @mutex.lock
52
+ begin
53
+ target = next_subscriber
54
+ send_message(target, message) if target
55
+ ensure
56
+ @mutex.unlock rescue nil
57
+ end
58
+ end
59
+
60
+ def broadcast(message)
61
+ send_message(subscribers, message)
62
+ end
63
+
64
+ # Send a message to the specified target
65
+ def send_message(target, message)
66
+ # Array-ize unless we're an Enumerable already that isn't a Mailbox
67
+ target = [target] unless target.is_a?(Enumerable) && !target.respond_to?(:receive)
68
+
69
+ target.each do |targ|
70
+ begin
71
+ targ << message
72
+ rescue Celluloid::MailboxError
73
+ # Mailbox died, remove subscriber
74
+ remove_subscriber targ
75
+ end
76
+ end
77
+ nil
78
+ end
79
+ end
80
+
81
+ # Randomly route messages to workers
82
+ class Random < Base
83
+ def next_subscriber
84
+ subscribers.sample
85
+ end
86
+ end
87
+
88
+ # Basic router using a RoundRobin strategy
89
+ class RoundRobin < Base
90
+ # Signal new work to all subscribers/waiters
91
+ def next_subscriber
92
+ subscribers.rotate!
93
+ subscribers.last
94
+ end
95
+ end
96
+
97
+ # Send message to the worker with the smallest mailbox
98
+ class SmallestMailbox < Base
99
+ def next_subscriber
100
+ subscribers.sort { |a,b| a.size <=> b.size }.first
101
+ end
102
+ end
103
+
104
+ # The strategy employed is similar to a ScatterGatherFirstCompleted router in
105
+ # Akka, but wrapping messages in the ForwardedCall structure so computation is
106
+ # only completed once.
107
+ class FirstAvailable < Base
108
+ def mailbox
109
+ @mailbox ||= Celluloid::Mailbox.new
110
+ end
111
+
112
+ def <<(msg)
113
+ @mutex.lock
114
+ begin
115
+ mailbox << msg
116
+ send_message(subscribers, WindUp::ForwardedCall.new(mailbox))
117
+ ensure
118
+ @mutex.unlock rescue nil
119
+ end
120
+ end
121
+
122
+ def shutdown
123
+ mailbox.shutdown
124
+ end
125
+ end
126
+ end
127
+
128
+ Routers.register :first_avaialble, Router::FirstAvailable
129
+ Routers.register :round_robin, Router::RoundRobin
130
+ Routers.register :random, Router::Random
131
+ Routers.register :smallest_mailbox, Router::SmallestMailbox
132
+ end
@@ -0,0 +1,3 @@
1
+ module WindUp
2
+ VERSION = "0.0.2"
3
+ end
data/lib/wind_up.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'celluloid'
2
+ require 'wind_up/exceptions'
3
+ require 'wind_up/calls'
4
+ require 'wind_up/routers'
5
+ require 'wind_up/queue_proxy'
6
+ require 'wind_up/queue_manager'
7
+ require 'wind_up/version'
8
+ require 'wind_up/celluloid_ext'
9
+
10
+ module WindUp
11
+ def self.logger
12
+ Celluloid.logger
13
+ end
14
+
15
+ def self.logger=(logger)
16
+ Celluloid.logger = logger
17
+ end
18
+ end
19
+
20
+ require 'wind_up/railtie' if defined?(::Rails)
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'pry'
3
+ rescue LoadError
4
+ end
5
+
6
+ require 'celluloid'
7
+ require 'wind_up'
8
+
9
+ Celluloid.shutdown; Celluloid.boot
10
+
11
+ def mute_celluloid_logging
12
+ # Temporarily mute celluloid logger
13
+ Celluloid.logger.level = Logger::FATAL
14
+ yield if block_given?
15
+ # Restore celluloid logger
16
+ Celluloid.logger.level = Logger::DEBUG
17
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ class FakeWorker
4
+ include Celluloid
5
+ def process(queue = nil)
6
+ if queue
7
+ queue << :done
8
+ else
9
+ :done
10
+ end
11
+ end
12
+
13
+ def crash
14
+ raise StandardError, "zomgcrash"
15
+ end
16
+ end
17
+
18
+ class SleepyWorker < FakeWorker
19
+ def sleepy
20
+ sleep 0.2
21
+ :done
22
+ end
23
+ end
24
+
25
+
26
+ describe WindUp::QueueManager do
27
+ let(:queue) { FakeWorker.queue size: 2 }
28
+ describe '#initialize' do
29
+ it 'creates a supervision group of workers' do
30
+ expect { FakeWorker.queue size: 1 }.to change { Celluloid::Actor.all.size }.by(2)
31
+ end
32
+
33
+ it 'creates as many workers as number of cores on the system' do
34
+ cores = FakeWorker.queue
35
+ cores.__manager__.size.should eq Celluloid.cores
36
+ end
37
+
38
+ it 'requires a worker class' do
39
+ mute_celluloid_logging do
40
+ expect { WindUp::QueueManager.new }.to raise_exception
41
+ sleep 0.1
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '#terminate' do
47
+ it 'terminates the manager' do
48
+ queue.terminate
49
+ queue.should_not be_alive
50
+ end
51
+
52
+ it 'terminates the pool' do
53
+ expect{ queue.terminate }.to change { Celluloid::Actor.all.size }.by(0)
54
+ end
55
+ end
56
+
57
+ describe '#sync' do
58
+ it 'processs calls synchronously' do
59
+ queue.process.should be :done
60
+ end
61
+ end
62
+
63
+ describe '#async' do
64
+ it 'processs calls asynchronously' do
65
+ q = Queue.new
66
+ queue.async.process(q)
67
+ q.pop.should be :done
68
+ end
69
+
70
+ it 'processes additional work when workers as sleeping' do
71
+ sleepy_queue = SleepyWorker.queue size: 1
72
+ start_time = Time.now
73
+ vals = 2.times.map { sleepy_queue.future.sleepy }
74
+ vals.each { |v| v.value }
75
+ (start_time - Time.now).should be < 0.3
76
+ end
77
+
78
+ it 'handles crashed calls gracefully' do
79
+ mute_celluloid_logging do
80
+ queue.async.crash
81
+ queue.should be_alive
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#future' do
87
+ it 'processes calls as futures' do
88
+ f = queue.future.process
89
+ f.value.should be :done
90
+ end
91
+ end
92
+
93
+ describe '#size=' do
94
+ let(:manager) { queue.__manager__ }
95
+ it 'increases the size of the pool' do
96
+ manager.size.should eq 2
97
+ expect { manager.size = 3 }.to change{ Celluloid::Actor.all.size }.by(1)
98
+ end
99
+
100
+ it 'reduces the size of the pool' do
101
+ manager.size.should eq 2
102
+ expect { manager.size = 1 }.to change{ Celluloid::Actor.all.size }.by(-1)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,372 @@
1
+ require 'spec_helper'
2
+
3
+ class FakeWorker
4
+ include Celluloid
5
+ def perform(*args); end;
6
+ end
7
+
8
+ # Because we use a Singleton model, it's tough to change configurations
9
+ # without redefining a ton of classes. This class just helps us dynamically
10
+ # create Queue classes using a block.
11
+ class QueueFactory
12
+ ALPHABET = ('a'..'z').to_a
13
+ def self.bake(&block)
14
+ name = 10.times.map{ ALPHABET.sample }.join.capitalize
15
+ c = WindUp::Queue.new name
16
+
17
+ if block_given?
18
+ c.instance_eval &block
19
+ else
20
+ c.instance_eval {
21
+ worker_class FakeWorker
22
+ }
23
+ end
24
+ return c
25
+ end
26
+ end
27
+
28
+ # describe WindUp::Queue, pending: "rewrite" do
29
+ # let(:queue) { QueueFactory.bake }
30
+ # describe "#store" do
31
+ # context "by default" do
32
+ # it "sets the InMemory store" do
33
+ # queue.store.should be_a(WindUp::Store::InMemory)
34
+ # end
35
+ # end
36
+ # context "when passed :memory or 'memory'" do
37
+ # it "sets the InMemory store" do
38
+ # queue.store(:memory).should be_a(WindUp::Store::InMemory)
39
+ # queue.store("memory").should be_a(WindUp::Store::InMemory)
40
+ # queue.store.should be_a(WindUp::Store::InMemory)
41
+ # end
42
+ # end
43
+ # context "when passed :redis or 'redis'" do
44
+ # it "sets the Redis store" do
45
+ # queue.store(:redis).should be_a(WindUp::Store::Redis)
46
+ # queue.store("redis").should be_a(WindUp::Store::Redis)
47
+ # queue.store.should be_a(WindUp::Store::Redis)
48
+ # end
49
+
50
+ # context "with a :url provided" do
51
+ # it "uses the url to connect to Redis" do
52
+ # queue.store(:redis, url: "redis://127.0.0.1:6379")
53
+ # queue.store.should be
54
+ # end
55
+ # end
56
+
57
+ # context 'with :connection set to a Redis client instance' do
58
+ # it 'works' do
59
+ # redis = Redis.new
60
+ # queue.store(:redis, connection: redis)
61
+ # queue.store.should be
62
+ # end
63
+ # end
64
+
65
+ # context 'with :connection set to a Redis connection_pool' do
66
+ # it 'works' do
67
+ # redis = ConnectionPool.new(size: 2, timeout: 5) { Redis.new }
68
+ # queue.store(:redis, connection: redis)
69
+ # queue.store.should be
70
+ # end
71
+ # end
72
+ # end
73
+ # end
74
+
75
+ # context '#pool' do
76
+ # context 'configured using :workers/:worker' do
77
+ # let(:queue) do
78
+ # QueueFactory.bake do
79
+ # worker_class FakeWorker
80
+ # workers 3
81
+ # end
82
+ # end
83
+
84
+ # it "sets up the pool" do
85
+ # queue.pool.should be
86
+ # queue.pool.size.should eq 3
87
+ # end
88
+ # end
89
+
90
+ # context ':pool_name' do
91
+ # context 'when given an existing pool' do
92
+ # it 'uses the existing pool' do
93
+ # Celluloid::Actor[:pool_name_spec] = FakeWorker.pool size: 2
94
+ # queue = QueueFactory.bake do
95
+ # pool_name :pool_name_spec
96
+ # end
97
+ # queue.pool.should be Celluloid::Actor[:pool_name_spec]
98
+ # end
99
+ # end
100
+ # end
101
+ # end
102
+
103
+ # context '#priority_level' do
104
+ # context 'that are equal' do
105
+ # it 'creates a queue with priority levels' do
106
+ # queue = QueueFactory.bake do
107
+ # worker_class FakeWorker
108
+ # priority_level :clone1
109
+ # priority_level :clone2
110
+ # end
111
+
112
+ # queue.should be
113
+ # queue.priority_levels.should_not be_empty
114
+ # end
115
+ # end
116
+
117
+ # context 'that are weighted' do
118
+ # it 'creates a queue with priority levels' do
119
+ # queue = QueueFactory.bake do
120
+ # worker_class FakeWorker
121
+ # priority_level :high, weight: 10
122
+ # priority_level :low, weight: 1
123
+ # end
124
+
125
+ # queue.should be
126
+ # queue.priority_levels.should_not be_empty
127
+ # queue.priority_level_weights.should eq({high: 10, low: 1})
128
+ # end
129
+ # end
130
+
131
+ # context 'that are strictly ordered' do
132
+ # it 'creates a queue with priority levels' do
133
+ # queue = QueueFactory.bake do
134
+ # worker_class FakeWorker
135
+ # strict true
136
+ # priority_level :clone1
137
+ # priority_level :clone2
138
+ # end
139
+
140
+ # queue.should be
141
+ # queue.priority_levels.should_not be_empty
142
+ # queue.should be_strict
143
+ # end
144
+ # end
145
+
146
+ # it 'does not create priority levels with the same name' do
147
+ # queue = QueueFactory.bake do
148
+ # worker_class FakeWorker
149
+ # strict true
150
+ # priority_level :clone1
151
+ # priority_level :clone1
152
+ # end
153
+
154
+ # queue.priority_levels.should eq Set[:clone1]
155
+ # end
156
+ # end # with priority leels
157
+
158
+ # describe '#push' do
159
+ # context 'when not given a priority level' do
160
+ # it 'pushes the argument to the store with default priority' do
161
+ # queue.store.should_receive(:push).with("test", priority_level: nil)
162
+ # queue.push "test"
163
+ # end
164
+ # end
165
+ # context 'when given a priority level' do
166
+ # it 'pushes the argument to the store with specified priority' do
167
+ # queue.store.should_receive(:push).with("test", priority_level: "high")
168
+ # queue.push "test", priority_level: "high"
169
+ # end
170
+ # end
171
+ # end
172
+
173
+ # describe '#pop' do
174
+ # context 'without a priority level argument' do
175
+ # context 'with a strictly ordered queue' do
176
+ # it 'pops priority levels in order' do
177
+ # queue = QueueFactory.bake do
178
+ # worker_class FakeWorker
179
+ # strict true
180
+ # priority_level :high
181
+ # priority_level :low
182
+ # end
183
+ # queue.store.should_receive(:pop).with([:high, :low]).at_least(1).times
184
+ # queue.pop
185
+ # end
186
+ # end
187
+ # context 'with a loosely ordered queue' do
188
+ # it 'pops priority levels in proportion' do
189
+ # queue = QueueFactory.bake do
190
+ # worker_class FakeWorker
191
+ # priority_level :high
192
+ # priority_level :low
193
+ # end
194
+ # queue.store.stub(:pop) { nil }
195
+ # queue.store.stub(:pop).with { [:high, :low] }.and_return { "success" }
196
+ # queue.store.stub(:pop).with { [:low, :high] }.and_return { "success" }
197
+
198
+ # queue.pop.should be
199
+ # end
200
+ # end
201
+ # end
202
+ # context 'with a priority_level argument' do
203
+ # it 'pops the specified priority' do
204
+ # queue.store.stub(:pop) { nil }
205
+ # queue.store.stub(:pop).with(["queue"]) { "work" }
206
+ # queue.pop(["queue"]).should be
207
+ # end
208
+ # end
209
+ # end
210
+
211
+ # describe '#workers' do
212
+ # it 'returns the number of workers in the pool' do
213
+ # queue = QueueFactory.bake do
214
+ # worker_class FakeWorker
215
+ # workers 2
216
+ # end
217
+ # queue.workers.should eq 2
218
+ # end
219
+ # end
220
+
221
+ # describe '#busy_workers' do
222
+ # it 'returns the number of busy_workers in the pool' do
223
+ # queue = QueueFactory.bake do
224
+ # worker_class FakeWorker
225
+ # workers 2
226
+ # end
227
+ # queue.busy_workers.should eq 0
228
+ # end
229
+ # end
230
+
231
+ # describe '#idle_workers' do
232
+ # it 'returns the number of idle_workers in the pool' do
233
+ # queue = QueueFactory.bake do
234
+ # worker_class FakeWorker
235
+ # workers 2
236
+ # end
237
+ # queue.idle_workers.should eq 2
238
+ # end
239
+ # end
240
+
241
+ # describe '#backlog?' do
242
+ # it 'returns true if the pool is fully utilized' do
243
+ # queue = QueueFactory.bake do
244
+ # worker_class FakeWorker
245
+ # end
246
+ # queue.should_not be_backlog
247
+ # end
248
+ # end
249
+
250
+ # describe '#size' do
251
+ # it 'returns the number of jobs in the queue store' do
252
+ # queue.store.should_receive(:size).and_return(4)
253
+ # queue.size.should eq 4
254
+ # end
255
+ # end
256
+
257
+ # describe '#strict?' do
258
+ # context 'when the queue is strictly ordered' do
259
+ # it 'returns true' do
260
+ # queue = QueueFactory.bake do
261
+ # worker_class FakeWorker
262
+ # strict true
263
+ # end
264
+ # queue.should be_strict
265
+ # end
266
+ # end
267
+ # context 'when the queue is not strictly ordered' do
268
+ # it 'returns false' do
269
+ # queue = QueueFactory.bake do
270
+ # worker_class FakeWorker
271
+ # end
272
+ # queue.should_not be_strict
273
+ # end
274
+ # end
275
+ # end
276
+
277
+ # describe '#priority_levels' do
278
+ # context 'with a queue with priority levels' do
279
+ # it 'returns the priority levels for this queue' do
280
+ # queue = QueueFactory.bake do
281
+ # worker_class FakeWorker
282
+ # priority_level :high
283
+ # priority_level :low
284
+ # end
285
+ # queue.priority_levels.should eq Set[:high, :low]
286
+ # end
287
+
288
+ # it 'does not return duplicates' do
289
+ # queue = QueueFactory.bake do
290
+ # worker_class FakeWorker
291
+ # priority_level :high, weight: 10
292
+ # priority_level :low, weight: 1
293
+ # end
294
+ # queue.priority_levels.should eq Set[:high, :low]
295
+ # end
296
+ # end
297
+ # context 'with a queue without priority levels' do
298
+ # it 'returns an empty array' do
299
+ # queue = QueueFactory.bake do
300
+ # worker_class FakeWorker
301
+ # end
302
+ # queue.priority_levels.should eq Set[]
303
+ # end
304
+ # end
305
+ # end
306
+
307
+ # describe '#priority_level_weights' do
308
+ # context 'when run on a queue without priority levels' do
309
+ # it 'returns an empty hash' do
310
+ # queue = QueueFactory.bake do
311
+ # worker_class FakeWorker
312
+ # end
313
+ # queue.priority_level_weights.should eq({})
314
+ # end
315
+ # end
316
+
317
+ # context 'when run on a queue with unweighted priority levels' do
318
+ # it "returns the priority levels with their weightings" do
319
+ # queue = QueueFactory.bake do
320
+ # worker_class FakeWorker
321
+ # priority_level :high
322
+ # priority_level :low
323
+ # end
324
+ # queue.priority_level_weights.should eq({high: 1, low: 1})
325
+ # end
326
+ # end
327
+
328
+ # context 'when run on a queue with unweighted priority levels' do
329
+ # it "returns the priority levels with their weightings" do
330
+ # queue = QueueFactory.bake do
331
+ # worker_class FakeWorker
332
+ # priority_level :high, weight: 10
333
+ # priority_level :low, weight: 1
334
+ # end
335
+ # queue.priority_level_weights.should eq({high: 10, low: 1})
336
+ # end
337
+ # end
338
+ # end
339
+
340
+ # describe '#default_priority_level' do
341
+ # context 'when run on a queue without priority levels' do
342
+ # it 'returns nil' do
343
+ # queue = QueueFactory.bake do
344
+ # worker_class FakeWorker
345
+ # end
346
+ # queue.default_priority_level.should_not be
347
+ # end
348
+ # end
349
+
350
+ # context 'when run on a queue with priority levels' do
351
+ # it "returns the first priority level" do
352
+ # queue = QueueFactory.bake do
353
+ # worker_class FakeWorker
354
+ # priority_level :high
355
+ # priority_level :low
356
+ # end
357
+ # queue.default_priority_level.should eq(:high)
358
+ # end
359
+ # end
360
+
361
+ # context 'when run on a queue with a default priority level explicitly set' do
362
+ # it "returns the set priority level" do
363
+ # queue = QueueFactory.bake do
364
+ # worker_class FakeWorker
365
+ # priority_level :high, weight: 10
366
+ # priority_level :low, weight: 1, default: true
367
+ # end
368
+ # queue.default_priority_level.should eq(:low)
369
+ # end
370
+ # end
371
+ # end
372
+ # end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'a router' do
4
+ describe '#add_subscriber' do
5
+ it 'adds a subscriber to the list of subscribers' do
6
+ subject.add_subscriber(subscriber)
7
+ subject.subscribers.should include(subscriber)
8
+ end
9
+ end
10
+
11
+ describe '#<<' do
12
+ it 'forwards messages to a subscriber' do
13
+ subject.add_subscriber subscriber
14
+ subject << :ok
15
+ subscriber.receive(0).should be :ok
16
+ end
17
+ end
18
+ end
19
+
20
+ describe WindUp::Routers do
21
+ let(:subscriber) { Celluloid::Mailbox.new }
22
+ let(:origin) { WindUp::Routers[router_class].new }
23
+ subject { origin }
24
+
25
+ describe WindUp::Router::RoundRobin do
26
+ let(:router_class) { :round_robin }
27
+ it_behaves_like 'a router'
28
+ end
29
+
30
+ describe WindUp::Router::Random do
31
+ let(:router_class) { :random }
32
+ it_behaves_like 'a router'
33
+ end
34
+
35
+ describe WindUp::Router::SmallestMailbox do
36
+ let(:router_class) { :smallest_mailbox }
37
+ it_behaves_like 'a router'
38
+ end
39
+
40
+ describe WindUp::Router::FirstAvailable do
41
+ let(:router_class) { :first_avaialble }
42
+
43
+ describe '#add_subscriber' do
44
+ it 'adds a subscriber to the list of subscribers' do
45
+ subject.add_subscriber(subscriber)
46
+ subject.subscribers.should include(subscriber)
47
+ end
48
+ end
49
+
50
+ describe '#<<' do
51
+ it 'publishes the Forwarder event to all subscribers' do
52
+ origin.add_subscriber(subscriber)
53
+ origin << "New work"
54
+ subscriber.receive(1).should be_a(WindUp::ForwardedCall)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe WindUp do
4
+ describe '.logger' do
5
+ it "delegates get to Celluloid's logger" do
6
+ WindUp.logger.should == Celluloid.logger
7
+ end
8
+
9
+ it "delegates set to Celluloid's logger" do
10
+ Celluloid.should_receive(:logger=)
11
+ WindUp.logger = nil
12
+ end
13
+ end
14
+ end
data/wind_up.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'wind_up/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "wind_up"
8
+ gem.version = WindUp::VERSION
9
+ gem.authors = ["Ryan Chan"]
10
+ gem.email = ["ryan@ryanlchan.com"]
11
+ gem.summary = %q{A drop-in replacement for Celluloid pools using the message API}
12
+ gem.description = %q{WindUp is a simple background processing library meant to improve on Celluloid's pools}
13
+ gem.homepage = "https://github.com/ryanlchan/wind_up"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency "rspec"
21
+ gem.add_development_dependency "rake"
22
+
23
+ gem.add_dependency "celluloid", "~> 0.14.1"
24
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wind_up
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Chan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: celluloid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.14.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.14.1
55
+ description: WindUp is a simple background processing library meant to improve on
56
+ Celluloid's pools
57
+ email:
58
+ - ryan@ryanlchan.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .ruby-gemset
64
+ - .ruby-version
65
+ - CHANGES.md
66
+ - Gemfile
67
+ - Gemfile.lock
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - lib/wind_up.rb
72
+ - lib/wind_up/calls.rb
73
+ - lib/wind_up/celluloid_ext.rb
74
+ - lib/wind_up/exceptions.rb
75
+ - lib/wind_up/queue_manager.rb
76
+ - lib/wind_up/queue_proxy.rb
77
+ - lib/wind_up/railtie.rb
78
+ - lib/wind_up/routers.rb
79
+ - lib/wind_up/version.rb
80
+ - spec/spec_helper.rb
81
+ - spec/wind_up/queue_manager_spec.rb
82
+ - spec/wind_up/queue_spec.rb
83
+ - spec/wind_up/routers_spec.rb
84
+ - spec/wind_up_spec.rb
85
+ - wind_up.gemspec
86
+ homepage: https://github.com/ryanlchan/wind_up
87
+ licenses: []
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.0.3
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: A drop-in replacement for Celluloid pools using the message API
109
+ test_files:
110
+ - spec/spec_helper.rb
111
+ - spec/wind_up/queue_manager_spec.rb
112
+ - spec/wind_up/queue_spec.rb
113
+ - spec/wind_up/routers_spec.rb
114
+ - spec/wind_up_spec.rb