disruptor 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4a35b494617ac7e18df43e17b591458e04387d3e
4
+ data.tar.gz: a765278e3f12b61f4d441a6b4670c5ca15554d7b
5
+ SHA512:
6
+ metadata.gz: 5addfcfb1602ef8e119ab131ed412908e367d23bfd7b8e2d08d8998e184076f9514e65c7d579b4a0e489977406b248c34e2b389faa7d409404e1f22870cf1e4c
7
+ data.tar.gz: de42b8177aa049e903a1d7c2ae35a653dda549d46cc065ad8e536565af733728919ae2a073849b241697661584fe01688aa1511e02def4dc922fc14121d7bae2
@@ -0,0 +1,2 @@
1
+ .rbx/
2
+ pkg/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -f d
@@ -0,0 +1,12 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'disruptor.gemspec'
4
+
5
+ LineLength:
6
+ Enabled: false
7
+
8
+ Style/SignalException:
9
+ Enabled: false
10
+
11
+ Documentation:
12
+ Enabled: false
@@ -0,0 +1 @@
1
+ disruptor
@@ -0,0 +1 @@
1
+ 2.2.0
@@ -0,0 +1,12 @@
1
+ sudo: false
2
+
3
+ cache: :bundler
4
+
5
+ language: ruby
6
+
7
+ rvm:
8
+ - 2.0.0
9
+ - 2.1.0
10
+ - 2.2.0
11
+ - jruby-1.7.18
12
+ - rbx-2.5.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rspec'
4
+ gem 'rubocop', platform: :ruby
5
+
6
+ gemspec
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ disruptor (1.0.0.beta2)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.0.0)
10
+ astrolabe (1.3.0)
11
+ parser (>= 2.2.0.pre.3, < 3.0)
12
+ concurrent-ruby (0.8.0)
13
+ ref (~> 1.0, >= 1.0.5)
14
+ concurrent-ruby (0.8.0-java)
15
+ diff-lcs (1.2.5)
16
+ parser (2.2.0.2)
17
+ ast (>= 1.1, < 3.0)
18
+ powerpack (0.0.9)
19
+ rainbow (2.0.0)
20
+ rake (10.4.2)
21
+ ref (1.0.5)
22
+ rspec (3.2.0)
23
+ rspec-core (~> 3.2.0)
24
+ rspec-expectations (~> 3.2.0)
25
+ rspec-mocks (~> 3.2.0)
26
+ rspec-core (3.2.0)
27
+ rspec-support (~> 3.2.0)
28
+ rspec-expectations (3.2.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.2.0)
31
+ rspec-mocks (3.2.0)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.2.0)
34
+ rspec-support (3.2.0)
35
+ rubocop (0.28.0)
36
+ astrolabe (~> 1.3)
37
+ parser (>= 2.2.0.pre.7, < 3.0)
38
+ powerpack (~> 0.0.6)
39
+ rainbow (>= 1.99.1, < 3.0)
40
+ ruby-progressbar (~> 1.4)
41
+ ruby-progressbar (1.7.1)
42
+
43
+ PLATFORMS
44
+ java
45
+ ruby
46
+
47
+ DEPENDENCIES
48
+ bundler (~> 1.7)
49
+ concurrent-ruby
50
+ disruptor!
51
+ rake (~> 10.0)
52
+ rspec
53
+ rubocop
@@ -0,0 +1,35 @@
1
+ [![Build Status](https://travis-ci.org/ileitch/disruptor.svg?branch=master)](https://travis-ci.org/ileitch/disruptor)
2
+
3
+ # The LMAX Disruptor in Ruby.
4
+
5
+ The reference Java implementation is more of a framework than a pattern. I have simply taken the core concepts of the Disruptor.
6
+
7
+ The code may serve as a handy companion for Ruby developers digging into the [Disruptor Technical Paper](http://disruptor.googlecode.com/files/Disruptor-1.0.pdf).
8
+
9
+ There is a simple Queue implementation, if you're after a lock-free queue.
10
+
11
+ ### Wait Strategies
12
+
13
+ * `BusySpinWaitStrategy` - Spins until the sequence reaches the required value. CPU intensive but provides best throughput and latency. Do not use if there are more threads than logical CPU cores.
14
+ * `BlockingWaitStrategy` - Puts waiting threads to sleep. Use this strategy if you have more threads than logical cores.
15
+
16
+ ```
17
+ buffer = Disruptor::RingBuffer.new(20, BusySpinWaitStrategy.new)
18
+ ```
19
+
20
+ ### Cache-line Padding
21
+
22
+ One neat optimization the LMAX developers have used is cache-line padding of their Sequence object. Replicating this in Ruby is a little tricky as Ruby does not support native types. The problem is made even more tricky when you take into account the different internal Object structure across Ruby implementations. Perhaps a C-ext could achieve this, VM level support would be even better. Patches welcome! ;)
23
+
24
+ ### Benchmarks
25
+
26
+ I'm not going to show any results here, because they're pretty meaningless. No Ruby implementation can get close to performance of the Java implementation. If you do want to use the Disruptor pattern in JRuby, you're probably better off writing an extension for the official Disruptor.
27
+
28
+ Saying that, there are a couple of simple Queue benchmarks in `bm`.
29
+
30
+ ### TODO
31
+
32
+ * Detect buffer wrap around, have the publishers wait.
33
+ * Implement different processor wait strategies.
34
+ * Implement different publisher claim strategies.
35
+ * Implement cache-line padding (possible on MRI, Rubinius?).
@@ -0,0 +1,12 @@
1
+ require 'rake'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ Dir['lib/tasks/*.rake'].each { |rake| load rake }
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ if RUBY_VERSION > '1.9' && defined?(RUBY_ENGINE) && %w(rbx ruby).include?(RUBY_ENGINE)
9
+ task default: %w(spec rubocop)
10
+ else
11
+ task default: 'spec'
12
+ end
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
+ require 'disruptor'
3
+ require 'benchmark'
4
+ require 'thread'
5
+
6
+ width = 14
7
+ n = 6_000_000
8
+
9
+ Benchmark.bm(width) do |x|
10
+ disruptor = Disruptor::Queue.new(n, Disruptor::BusySpinWaitStrategy.new)
11
+ queue = Queue.new
12
+
13
+ n.times do
14
+ disruptor.push(nil)
15
+ queue.push(nil)
16
+ end
17
+
18
+ x.report('disruptor:') do
19
+ n.times { disruptor.pop }
20
+ end
21
+
22
+ x.report(' queue:') do
23
+ n.times { queue.pop }
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
+ require 'disruptor'
3
+ require 'benchmark'
4
+ require 'thread'
5
+
6
+ width = 14
7
+ n = 6_000_000
8
+
9
+ Benchmark.bm(width) do |x|
10
+ disruptor = Disruptor::Queue.new(n, Disruptor::BusySpinWaitStrategy.new)
11
+ queue = Queue.new
12
+
13
+ n.times do
14
+ queue.push(nil)
15
+ disruptor.push(nil)
16
+ end
17
+
18
+ x.report('disruptor:') do
19
+ threads = []
20
+ threads << Thread.new { (n / 2).times { disruptor.pop } }
21
+ threads << Thread.new { (n / 2).times { disruptor.pop } }
22
+ threads.map(&:join)
23
+ end
24
+
25
+ x.report(' queue:') do
26
+ threads = []
27
+ threads << Thread.new { (n / 2).times { queue.pop } }
28
+ threads << Thread.new { (n / 2).times { queue.pop } }
29
+ threads.map(&:join)
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'disruptor/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'disruptor'
8
+ spec.version = Disruptor::VERSION
9
+ spec.authors = ['Ian Leitch']
10
+ spec.email = ['port001@gmail.com']
11
+ spec.summary = %q(Basic implementation of the LMAX Disruptor pattern in Ruby.)
12
+ spec.homepage = 'https://github.com/ileitch/disruptor'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.7'
21
+ spec.add_development_dependency 'rake', '~> 10.0'
22
+ spec.add_development_dependency 'concurrent-ruby'
23
+
24
+ spec.platform = 'java' if defined? JRUBY_VERSION
25
+ end
@@ -0,0 +1,16 @@
1
+ module Disruptor
2
+ class BufferSizeError < StandardError; end
3
+ end
4
+
5
+ require 'concurrent'
6
+ require 'thread'
7
+
8
+ require 'disruptor/wait_strategy'
9
+ require 'disruptor/busy_spin_wait_strategy'
10
+ require 'disruptor/blocking_wait_strategy'
11
+ require 'disruptor/ring_buffer'
12
+ require 'disruptor/sequence'
13
+ require 'disruptor/processor'
14
+ require 'disruptor/processor_barrier'
15
+ require 'disruptor/processor_pool'
16
+ require 'disruptor/queue'
@@ -0,0 +1,27 @@
1
+ module Disruptor
2
+ #
3
+ # Implements a lock based wait strategy.
4
+ #
5
+ # Use when CPU resources are more of a concern than throughput and latency.
6
+ #
7
+ # Blocking causes a context switch when the thread sleeps, consider the
8
+ # BusySpinWaitStrategy instead if you expect the wait time to be less than
9
+ # the time it takes perform a context switch.
10
+ #
11
+ # This strategy is preferred if you have more threads than logical cores.
12
+ #
13
+ class BlockingWaitStrategy < WaitStrategy
14
+ def initialize
15
+ @mutex = Mutex.new
16
+ @cond = ConditionVariable.new
17
+ end
18
+
19
+ def wait_for(cursor, sequence)
20
+ @mutex.synchronize { @cond.wait(@mutex) } while cursor.get < sequence
21
+ end
22
+
23
+ def notify_blocked
24
+ @mutex.synchronize { @cond.broadcast }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module Disruptor
2
+ #
3
+ # Implements a Busy Spin wait strategy.
4
+ #
5
+ # Use when throughput and latency are of more concern than CPU resources.
6
+ #
7
+ # Typically this strategy is preferred when the number of threads is <= the
8
+ # number of logical cores.
9
+ #
10
+ class BusySpinWaitStrategy < WaitStrategy
11
+ def wait_for(cursor, sequence)
12
+ while cursor.get < sequence; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Disruptor
2
+ #
3
+ # Include this class into your event processors.
4
+ # Your processor must implement process_event(event).
5
+ #
6
+ module Processor
7
+ class Stop < StandardError; end
8
+
9
+ def self.method_added(name)
10
+ if name.to_sym == :setup && self != Disruptor::Processor
11
+ raise 'Do not override setup in your processor subclass.'
12
+ end
13
+ end
14
+
15
+ def setup(buffer, barrier, sequence)
16
+ @buffer = buffer
17
+ @barrier = barrier
18
+ @sequence = sequence
19
+ end
20
+
21
+ def start
22
+ @thread = Thread.new do
23
+ loop do
24
+ begin
25
+ process_next_sequence
26
+ rescue Stop
27
+ break
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def process_next_sequence
34
+ next_sequence = @sequence.increment
35
+ @barrier.wait_for(next_sequence)
36
+ event = @buffer.get(next_sequence)
37
+ raise event if event == Stop
38
+ process_event(event)
39
+ end
40
+
41
+ def stop
42
+ seq = @buffer.claim
43
+ @buffer.set(seq, Stop)
44
+ @buffer.commit(seq)
45
+
46
+ @thread.join if @thread
47
+ end
48
+
49
+ def process_event(event) # rubocop:disable Lint/UnusedMethodArgument
50
+ raise NotImplementedError
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ module Disruptor
2
+ #
3
+ # This class implements a thread-safe read barrier between the buffer
4
+ # and Processors.
5
+ #
6
+ # Processors ask the barrier for the next sequence they want, the barrier
7
+ # spins waiting for the sequence to become available.
8
+ # This is achieved without CAS as the buffer's cursor is protected by a
9
+ # memory barrier.
10
+ #
11
+ # <- claimed ->
12
+ # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
13
+ # ^ ^
14
+ # cursor next
15
+ #
16
+ # In this example sequences 0, 1 and 2 are available for reading by
17
+ # the processors.
18
+ #
19
+ class ProcessorBarrier
20
+ def initialize(buffer, wait_strategy)
21
+ @buffer = buffer
22
+ @wait_strategy = wait_strategy
23
+ @last_known_sequence = Disruptor::RingBuffer::INITIAL_CURSOR_VALUE
24
+ end
25
+
26
+ def wait_for(sequence)
27
+ # Optimization:
28
+ # Store the last known cursor value in local memory to avoid
29
+ # going down into the primitive Sequence#get.
30
+ return if sequence < @last_known_sequence
31
+
32
+ @wait_strategy.wait_for(@buffer.cursor, sequence)
33
+
34
+ # TODO: Candidate for cache-line padding?
35
+ @last_known_sequence = @buffer.cursor.get
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ module Disruptor
2
+ #
3
+ # This class implements a collection of Processors that share a CAS
4
+ # protected incremental sequence.
5
+ #
6
+ # Processors request slots from the buffer in a gated fashion.
7
+ # Processors A, B, C ... will never contend for the same slot in the buffer.
8
+ #
9
+ class ProcessorPool
10
+ def initialize(buffer, wait_strategy)
11
+ @sequence = Sequence.new(Disruptor::RingBuffer::INITIAL_NEXT_VALUE)
12
+ @buffer = buffer
13
+ @barrier = ProcessorBarrier.new(@buffer, wait_strategy)
14
+ @processors = []
15
+ end
16
+
17
+ def add(processor)
18
+ processor.setup(@buffer, @barrier, @sequence)
19
+ @processors << processor
20
+ processor.start
21
+ end
22
+
23
+ def drain
24
+ @processors.map(&:stop)
25
+ @processors = []
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Disruptor
2
+ #
3
+ # A simple n-reader, n-writer queue.
4
+ #
5
+ class Queue
6
+ def initialize(size, wait_strategy)
7
+ @buffer = RingBuffer.new(size, wait_strategy)
8
+ @sequence = Sequence.new
9
+ @barrier = ProcessorBarrier.new(@buffer, wait_strategy)
10
+ end
11
+
12
+ def push(obj)
13
+ seq = @buffer.claim
14
+ @buffer.set(seq, obj)
15
+ @buffer.commit(seq)
16
+ nil
17
+ end
18
+ alias_method :<<, :push
19
+
20
+ def pop
21
+ next_sequence = @sequence.increment
22
+ @barrier.wait_for(next_sequence)
23
+ @buffer.get(next_sequence)
24
+ end
25
+
26
+ def size
27
+ @buffer.committed_count - @sequence.get
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,67 @@
1
+ module Disruptor
2
+ #
3
+ # This class implements an n-writer ring buffer.
4
+ #
5
+ # A publisher must first claim a slot in the buffer before it can write
6
+ # any data. This is achieved using a pointer to the next available slot
7
+ # which is incremented using CAS. Once the Publisher has written data
8
+ # it commits it (makes it visible to Processors) by setting a cursor
9
+ # pointer.
10
+ #
11
+ # Publishers commit in the same order they claim a slot, this is enforced
12
+ # for you by the buffer. For example, Publisher A has claimed slot 1 and
13
+ # Publisher B slot 2. Publisher A - for whatever reason - may take more
14
+ # time to commit the slot than Publisher B. In this scenario Publisher B
15
+ # will spin trying to set the cursor until A commits.
16
+ #
17
+ # Note that the actual position of data in the ring buffer is never known
18
+ # outside of the buffer. Publishers and Processors communicate with the
19
+ # buffer using a non-looping sequence. The buffer uses the sequence
20
+ # modulo the buffer size as the physical slot.
21
+ #
22
+ class RingBuffer
23
+ INITIAL_CURSOR_VALUE = -1
24
+ INITIAL_NEXT_VALUE = 0
25
+
26
+ attr_reader :cursor, :next
27
+
28
+ def initialize(size, wait_strategy, &blk)
29
+ raise BufferSizeError, 'Buffer size must be a power of two.' if size.odd?
30
+
31
+ @size = size
32
+ @wait_strategy = wait_strategy
33
+ @cursor = Sequence.new(INITIAL_CURSOR_VALUE)
34
+ @next = Sequence.new(INITIAL_NEXT_VALUE)
35
+ @buffer = Array.new(@size, &blk)
36
+ end
37
+
38
+ def claim
39
+ @next.increment
40
+ end
41
+
42
+ def commit(seq)
43
+ if @cursor.get != INITIAL_CURSOR_VALUE && seq != INITIAL_NEXT_VALUE
44
+ @wait_strategy.wait_for(@cursor, seq - 1)
45
+ end
46
+
47
+ @cursor.set(seq - 1, seq)
48
+ @wait_strategy.notify_blocked
49
+ end
50
+
51
+ def set(seq, event)
52
+ @buffer[seq % @size] = event
53
+ end
54
+
55
+ def get(seq)
56
+ @buffer[seq % @size]
57
+ end
58
+
59
+ def claimed_count
60
+ @next.get - @cursor.get - 1
61
+ end
62
+
63
+ def committed_count
64
+ @cursor.get + 1
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,25 @@
1
+ module Disruptor
2
+ class Sequence
3
+ INITIAL_VALUE = 0
4
+
5
+ def initialize(initial = INITIAL_VALUE)
6
+ @sequence = Concurrent::Atomic.new(initial)
7
+ end
8
+
9
+ def get
10
+ @sequence.get
11
+ end
12
+
13
+ def set(current_seq, new_seq)
14
+ until @sequence.compare_and_set(current_seq, new_seq); end
15
+ end
16
+
17
+ def increment
18
+ loop do
19
+ current_seq = @sequence.get
20
+ next_seq = current_seq + 1
21
+ return current_seq if @sequence.compare_and_set(current_seq, next_seq)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Disruptor
2
+ VERSION = '1.0.0.beta2'
3
+ end
@@ -0,0 +1,9 @@
1
+ module Disruptor
2
+ class WaitStrategy
3
+ def wait_for(sequence, slot) # rubocop:disable Lint/UnusedMethodArgument
4
+ raise NotImplementedError
5
+ end
6
+
7
+ def notify_blocked; end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ begin
2
+ require 'rubocop/rake_task'
3
+ t = RuboCop::RakeTask.new
4
+ t.options << '-D'
5
+ rescue LoadError
6
+ warn 'rubocop not available.'
7
+ task rubocop: []
8
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Disruptor::BlockingWaitStrategy do
4
+ let(:mutex) { double }
5
+ let(:condition) { double(broadcast: nil) }
6
+ let(:strategy) { Disruptor::BlockingWaitStrategy.new }
7
+ let(:sequence) { double }
8
+
9
+ before do
10
+ allow(mutex).to receive(:synchronize).and_yield
11
+ allow(Mutex).to receive_messages(new: mutex)
12
+ allow(ConditionVariable).to receive_messages(new: condition)
13
+ end
14
+
15
+ it 'returns when the sequence value reaches the given slot' do
16
+ allow(sequence).to receive_messages(get: 1)
17
+ strategy.wait_for(sequence, 1)
18
+ end
19
+
20
+ it 'sleeps if the sequence has not reached the given slot'
21
+
22
+ it 'notifies all blocked publishers' do
23
+ expect(condition).to receive(:broadcast)
24
+ strategy.notify_blocked
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Disruptor::BusySpinWaitStrategy do
4
+ let(:strategy) { Disruptor::BusySpinWaitStrategy.new }
5
+ let(:sequence) { double }
6
+
7
+ it 'returns when the sequence value reaches the given slot' do
8
+ allow(sequence).to receive_messages(get: 1)
9
+ strategy.wait_for(sequence, 1)
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Disruptor
2
+ class TestWaitStrategy < WaitStrategy
3
+ def wait_for(*); end
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Disruptor::ProcessorPool, 'add' do
4
+ let(:buffer) { double }
5
+ let(:pool) { Disruptor::ProcessorPool.new(buffer, Disruptor::TestWaitStrategy.new) }
6
+ let(:barrier) { double }
7
+ let(:sequence) { double }
8
+ let(:processor) { double(setup: nil, start: nil) }
9
+
10
+ before do
11
+ allow(Disruptor::ProcessorBarrier).to receive_messages(new: barrier)
12
+ allow(Disruptor::Sequence).to receive_messages(new: sequence)
13
+ end
14
+
15
+ it 'calls setup on the given processor' do
16
+ expect(processor).to receive(:setup).with(buffer, barrier, sequence)
17
+ pool.add(processor)
18
+ end
19
+
20
+ it 'starts the processor' do
21
+ expect(processor).to receive(:start)
22
+ pool.add(processor)
23
+ end
24
+ end
25
+
26
+ describe Disruptor::ProcessorPool, 'drain' do
27
+ let(:buffer) { double }
28
+ let(:pool) { Disruptor::ProcessorPool.new(buffer, Disruptor::TestWaitStrategy.new) }
29
+ let(:processor) { double(setup: nil, start: nil) }
30
+
31
+ before { pool.add(processor) }
32
+
33
+ it 'stops each processor' do
34
+ expect(processor).to receive(:stop)
35
+ pool.drain
36
+ end
37
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe Disruptor::Processor do
4
+ let(:processor_subclass) do
5
+ c = Class.new
6
+ c.send(:include, Disruptor::Processor)
7
+ c
8
+ end
9
+
10
+ it 'raises a NotImplementedError if the subclass does not implement handle_event' do
11
+ expect { processor_subclass.new.process_event(double) }.to raise_error(NotImplementedError)
12
+ end
13
+
14
+ it 'raises an error when a subclass overrides the setup method' do
15
+ expect { processor_subclass.define_method(:setup) {} }.to raise_error
16
+ end
17
+ end
18
+
19
+ describe Disruptor::Processor, 'process_next_sequence' do
20
+ class MyProcessor
21
+ include Disruptor::Processor
22
+ attr_accessor :processed_event
23
+ def process_event(e)
24
+ self.processed_event = e
25
+ end
26
+ end
27
+
28
+ let(:event) { double }
29
+ let(:sequence) { double(increment: 10) }
30
+ let(:buffer) { double(get: event) }
31
+ let(:barrier) { double(wait_for: nil, processor_stopping: nil) }
32
+ let(:processor) { MyProcessor.new }
33
+
34
+ before { processor.setup(buffer, barrier, sequence) }
35
+
36
+ it 'increments the sequence' do
37
+ expect(sequence).to receive(:increment)
38
+ processor.process_next_sequence
39
+ end
40
+
41
+ it 'waits for the next sequence' do
42
+ expect(barrier).to receive(:wait_for).with(10)
43
+ processor.process_next_sequence
44
+ end
45
+
46
+ it 'gets the event for the sequence' do
47
+ expect(buffer).to receive(:get).with(10)
48
+ processor.process_next_sequence
49
+ end
50
+
51
+ it 'dispatches the event processor' do
52
+ expect do
53
+ processor.process_next_sequence
54
+ end.to change(processor, :processed_event).from(nil).to(event)
55
+ end
56
+ end
57
+
58
+ describe Disruptor::Processor, 'stop' do
59
+ let(:processor_subclass) do
60
+ c = Class.new
61
+ c.send(:include, Disruptor::Processor)
62
+ c
63
+ end
64
+
65
+ let(:buffer) { double(claim: nil, set: nil, commit: nil) }
66
+ let(:thread) { double }
67
+ let(:processor) { processor_subclass.new }
68
+ let(:barrier) { double(processor_stopping: nil) }
69
+
70
+ before do
71
+ processor.setup(buffer, barrier, nil)
72
+ allow(Thread).to receive_messages(new: thread)
73
+ end
74
+
75
+ it 'claims a slot in the buffer for the Stop instruction' do
76
+ expect(buffer).to receive(:claim)
77
+ processor.stop
78
+ end
79
+
80
+ it 'adds a Stop instruction into the buffer' do
81
+ allow(buffer).to receive_messages(claim: 1)
82
+ expect(buffer).to receive(:set).with(1, Disruptor::Processor::Stop)
83
+ processor.stop
84
+ end
85
+
86
+ it 'commits the Stop instruction in the buffer' do
87
+ expect(buffer).to receive(:commit)
88
+ processor.stop
89
+ end
90
+
91
+ it 'joins the thread' do
92
+ processor.start
93
+ expect(thread).to receive(:join)
94
+ processor.stop
95
+ end
96
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Disruptor::Queue do
4
+ it 'returns the queue size' do
5
+ queue = Disruptor::Queue.new(6, Disruptor::TestWaitStrategy.new)
6
+ expect(queue.size).to eq(0)
7
+ 5.times { queue.push(nil) }
8
+ expect(queue.size).to eq(5)
9
+ 3.times { queue.pop }
10
+ expect(queue.size).to eq(2)
11
+ 2.times { queue.pop }
12
+ expect(queue.size).to eq(0)
13
+ end
14
+
15
+ describe 'with BusySpinWaitStrategy' do
16
+ let(:queue) { Disruptor::Queue.new(12, Disruptor::BusySpinWaitStrategy.new) }
17
+
18
+ it 'can push and pop an object' do
19
+ q = queue
20
+ t1 = Thread.new { 10.times { q.push(:data) } }
21
+ t2 = Thread.new { 10.times.map { q.pop } }
22
+ [t1, t2].map(&:join)
23
+ expect(t2.value).to eq([:data] * 10)
24
+ end
25
+ end
26
+
27
+ describe 'with BlockingWaitStrategy' do
28
+ let(:queue) { Disruptor::Queue.new(12, Disruptor::BlockingWaitStrategy.new) }
29
+
30
+ it 'can push and pop an object' do
31
+ q = queue
32
+ t1 = Thread.new { 10.times { q.push(:data) } }
33
+ t2 = Thread.new { 10.times.map { q.pop } }
34
+ [t1, t2].map(&:join)
35
+ expect(t2.value).to eq([:data] * 10)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe Disruptor::RingBuffer do
4
+ let(:buffer) { Disruptor::RingBuffer.new(32, Disruptor::TestWaitStrategy.new) }
5
+
6
+ it 'raises an error when initialized with a size that is not a power of two' do
7
+ expect { Disruptor::RingBuffer.new(31, Disruptor::TestWaitStrategy.new) }.to raise_error(Disruptor::BufferSizeError)
8
+ end
9
+
10
+ it 'accepts a block to preallocate the buffer' do
11
+ event = double
12
+ buffer = Disruptor::RingBuffer.new(6, Disruptor::TestWaitStrategy.new) { event }
13
+ expect(buffer.get(0)).to eq(event)
14
+ end
15
+
16
+ it 'returns the number of claimed slots' do
17
+ buffer = Disruptor::RingBuffer.new(20, Disruptor::TestWaitStrategy.new)
18
+ 5.times { buffer.claim }
19
+ 2.times { |i| buffer.commit(i) }
20
+ expect(buffer.claimed_count).to eq(3)
21
+ end
22
+
23
+ it 'returns the committed slots count' do
24
+ buffer = Disruptor::RingBuffer.new(20, Disruptor::TestWaitStrategy.new)
25
+ 5.times do
26
+ i = buffer.claim
27
+ buffer.commit(i)
28
+ end
29
+ expect(buffer.committed_count).to eq(5)
30
+ end
31
+ end
32
+
33
+ describe Disruptor::RingBuffer, 'claim' do
34
+ let(:buffer) { Disruptor::RingBuffer.new(20, Disruptor::TestWaitStrategy.new) }
35
+
36
+ it 'returns the next sequence' do
37
+ expect(buffer.claim).to eq(0)
38
+ expect(buffer.claim).to eq(1)
39
+ expect(buffer.claim).to eq(2)
40
+ end
41
+ end
42
+
43
+ describe Disruptor::RingBuffer, 'commit' do
44
+ let(:wait_strategy) { double(wait_for: nil, notify_blocked: nil) }
45
+ let(:buffer) { Disruptor::RingBuffer.new(12, wait_strategy) }
46
+ let(:cursor) { double(set: nil, get: Disruptor::RingBuffer::INITIAL_CURSOR_VALUE) }
47
+
48
+ before do
49
+ allow(Disruptor::Sequence).to receive_messages(new: cursor)
50
+ end
51
+
52
+ it 'waits for the cursor to reach the previous slot' do
53
+ allow(cursor).to receive_messages(get: 0)
54
+ expect(wait_strategy).to receive(:wait_for).with(cursor, 15)
55
+ buffer.commit(16)
56
+ end
57
+
58
+ it 'sets the cursor to the current slot' do
59
+ allow(cursor).to receive_messages(get: 0)
60
+ expect(cursor).to receive(:set).with(15, 16)
61
+ buffer.commit(16)
62
+ end
63
+
64
+ it 'sets the cursor to 0 for the first commit' do
65
+ expect(cursor).to receive(:set).with(-1, 0)
66
+ buffer.commit(0)
67
+ end
68
+
69
+ it 'does not wait for the cursor for the first commit into the buffer' do
70
+ expect(wait_strategy).not_to receive(:wait_for)
71
+ buffer.commit(0)
72
+ end
73
+
74
+ it 'notifies blocked publishers' do
75
+ expect(wait_strategy).to receive(:notify_blocked)
76
+ buffer.commit(0)
77
+ end
78
+ end
79
+
80
+ describe Disruptor::RingBuffer, 'get/set' do
81
+ let(:buffer) { Disruptor::RingBuffer.new(12, Disruptor::TestWaitStrategy.new) }
82
+ let(:event) { double }
83
+
84
+ it 'returns the event for the given seq' do
85
+ buffer.set(16, event)
86
+ expect(buffer.get(16)).to eq(event)
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
+ require 'disruptor'
3
+ require 'fixtures/test_wait_strategy'
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: disruptor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.beta2
5
+ platform: ruby
6
+ authors:
7
+ - Ian Leitch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - port001@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".rubocop.yml"
65
+ - ".ruby-gemset"
66
+ - ".ruby-version"
67
+ - ".travis.yml"
68
+ - Gemfile
69
+ - Gemfile.lock
70
+ - README.md
71
+ - Rakefile
72
+ - bm/one_processor.rb
73
+ - bm/two_processors.rb
74
+ - disruptor.gemspec
75
+ - lib/disruptor.rb
76
+ - lib/disruptor/blocking_wait_strategy.rb
77
+ - lib/disruptor/busy_spin_wait_strategy.rb
78
+ - lib/disruptor/processor.rb
79
+ - lib/disruptor/processor_barrier.rb
80
+ - lib/disruptor/processor_pool.rb
81
+ - lib/disruptor/queue.rb
82
+ - lib/disruptor/ring_buffer.rb
83
+ - lib/disruptor/sequence.rb
84
+ - lib/disruptor/version.rb
85
+ - lib/disruptor/wait_strategy.rb
86
+ - lib/tasks/rubocop.rake
87
+ - spec/blocking_wait_strategy_spec.rb
88
+ - spec/busy_spin_wait_strategy_spec.rb
89
+ - spec/fixtures/test_wait_strategy.rb
90
+ - spec/processor_pool_spec.rb
91
+ - spec/processor_spec.rb
92
+ - spec/queue_spec.rb
93
+ - spec/ring_buffer_spec.rb
94
+ - spec/spec_helper.rb
95
+ homepage: https://github.com/ileitch/disruptor
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">"
111
+ - !ruby/object:Gem::Version
112
+ version: 1.3.1
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.4.5
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Basic implementation of the LMAX Disruptor pattern in Ruby.
119
+ test_files:
120
+ - spec/blocking_wait_strategy_spec.rb
121
+ - spec/busy_spin_wait_strategy_spec.rb
122
+ - spec/fixtures/test_wait_strategy.rb
123
+ - spec/processor_pool_spec.rb
124
+ - spec/processor_spec.rb
125
+ - spec/queue_spec.rb
126
+ - spec/ring_buffer_spec.rb
127
+ - spec/spec_helper.rb