disruptor 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +12 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +53 -0
- data/README.md +35 -0
- data/Rakefile +12 -0
- data/bm/one_processor.rb +25 -0
- data/bm/two_processors.rb +31 -0
- data/disruptor.gemspec +25 -0
- data/lib/disruptor.rb +16 -0
- data/lib/disruptor/blocking_wait_strategy.rb +27 -0
- data/lib/disruptor/busy_spin_wait_strategy.rb +15 -0
- data/lib/disruptor/processor.rb +53 -0
- data/lib/disruptor/processor_barrier.rb +38 -0
- data/lib/disruptor/processor_pool.rb +28 -0
- data/lib/disruptor/queue.rb +30 -0
- data/lib/disruptor/ring_buffer.rb +67 -0
- data/lib/disruptor/sequence.rb +25 -0
- data/lib/disruptor/version.rb +3 -0
- data/lib/disruptor/wait_strategy.rb +9 -0
- data/lib/tasks/rubocop.rake +8 -0
- data/spec/blocking_wait_strategy_spec.rb +26 -0
- data/spec/busy_spin_wait_strategy_spec.rb +11 -0
- data/spec/fixtures/test_wait_strategy.rb +5 -0
- data/spec/processor_pool_spec.rb +37 -0
- data/spec/processor_spec.rb +96 -0
- data/spec/queue_spec.rb +38 -0
- data/spec/ring_buffer_spec.rb +88 -0
- data/spec/spec_helper.rb +3 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
disruptor
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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?).
|
data/Rakefile
ADDED
@@ -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
|
data/bm/one_processor.rb
ADDED
@@ -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
|
data/disruptor.gemspec
ADDED
@@ -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
|
data/lib/disruptor.rb
ADDED
@@ -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,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,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
|
data/spec/queue_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|