wind_up 0.0.2

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 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