slow_enumerator_tools 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5cecaee8660e212274a61072f434341d20318afd
4
- data.tar.gz: 953e06c5e5beb1bca606f7a6c95ab399d7967a46
2
+ SHA256:
3
+ metadata.gz: 7dde65640ab4d6809c1a3c77466e156f78cbf416e4eeb2349810a8ae4a0849a3
4
+ data.tar.gz: 0f11581710842aa47b6f28a79ccc5e4c9445cbea3daa2e02b78ee9943549751c
5
5
  SHA512:
6
- metadata.gz: e41bd66e2621550a7132d7b8e4f092a713cc6cab2cae3d29e4cd9b0f230d03bd914e5396bc5cfd070b446c06a37dafaa29dab322175234ee0221f68769171542
7
- data.tar.gz: ed7b14c899dba6e8c9ccc5b1142017f69b37bc5271867299ea06822a453fd0c4b111ca8141ae7b6db5c1844d87eb91a472b84f9c080e970e85a4f068fbb8ee55
6
+ metadata.gz: 324bf9924f0fe0900ee38708aa62186c75badaef9f5dbbb8aee2de85116a336cc17290ca489868cb78747c51dcc76ef102156fd98fbf6cff60975f282377bfc1
7
+ data.tar.gz: 7b1193a0691ae9b6fde6637f77efeda5c9bd46acd6e420e968a102086110d38cbf370010ae180d1c64b304a0b7e67b6e0e1358c6137b3ca42f53fa87a4a7dcf6
data/NEWS.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # SlowEnumeratorTools news
2
2
 
3
+ ## 1.1.0 (2017-11-08)
4
+
5
+ Fixes:
6
+
7
+ * Handled errors in `.merge` (#3)
8
+ * Handled errors in `.batch` (#2)
9
+
10
+ Features:
11
+
12
+ * Added `.buffer` (#1)
13
+
3
14
  ## 1.0.0 (2017-10-21)
4
15
 
5
16
  Changes:
6
17
 
7
- * Renamed buffer to batch
18
+ * Renamed `.buffer` to `.batch`
8
19
 
9
20
  ## 1.0.0a1 (2017-10-08)
10
21
 
data/README.md CHANGED
@@ -12,6 +12,8 @@ _SlowEnumeratorTools_ provides tools for transforming Ruby enumerators that prod
12
12
 
13
13
  * `SlowEnumeratorTools.batch`: given an enumerable, creates a new enumerable that yields batches containing all elements currently available.
14
14
 
15
+ * `SlowEnumeratorTools.buffer`: given an enumerable and a number, will create a buffer of that number of elements and try to fill it up with as many elements from that enumerable, so that they can be yielded immediately.
16
+
15
17
  ## Installation
16
18
 
17
19
  Add this line to your application's Gemfile:
@@ -95,6 +97,32 @@ p batch_enum.next
95
97
  p batch_enum.next
96
98
  ```
97
99
 
100
+ ### `SlowEnumeratorTools.buffer`
101
+
102
+ Given an enumerable and a number, will create a buffer of that number of elements and try to fill it up with as many elements from that enumerable.
103
+
104
+ This is particularly useful when reading from a slow source and writing to a slow sink, because the two will be able to work concurrently.
105
+
106
+ ```ruby
107
+ # Create (fake) articles enumerator
108
+ articles =
109
+ Enumerator.new do |y|
110
+ 5.times do |i|
111
+ sleep 1
112
+ y << "Article #{i}"
113
+ end
114
+ end
115
+
116
+ # Buffer
117
+ articles = SlowEnumeratorTools.buffer(articles, 5)
118
+
119
+ # Print each article
120
+ # This takes 6 seconds, rather than 10!
121
+ articles.each do |a|
122
+ sleep 1
123
+ end
124
+ ```
125
+
98
126
  ## Development
99
127
 
100
128
  Install dependencies:
@@ -8,8 +8,16 @@ module SlowEnumeratorTools
8
8
  def self.batch(es)
9
9
  SlowEnumeratorTools::Batcher.batch(es)
10
10
  end
11
+
12
+ def self.buffer(es, size)
13
+ SlowEnumeratorTools::Bufferer.buffer(es, size)
14
+ end
11
15
  end
12
16
 
13
17
  require_relative 'slow_enumerator_tools/version'
18
+
19
+ require_relative 'slow_enumerator_tools/util'
20
+
14
21
  require_relative 'slow_enumerator_tools/batcher'
22
+ require_relative 'slow_enumerator_tools/bufferer'
15
23
  require_relative 'slow_enumerator_tools/merger'
@@ -3,16 +3,9 @@
3
3
  module SlowEnumeratorTools
4
4
  module Batcher
5
5
  def self.batch(enum)
6
- q = Queue.new
7
- stop = Object.new
6
+ queue = Queue.new
8
7
 
9
- t =
10
- Thread.new do
11
- enum.each do |e|
12
- q << e
13
- end
14
- q << stop
15
- end
8
+ t = SlowEnumeratorTools::Util.gen_collector_thread(enum, queue)
16
9
 
17
10
  Enumerator.new do |y|
18
11
  loop do
@@ -20,20 +13,26 @@ module SlowEnumeratorTools
20
13
  ended = false
21
14
 
22
15
  # pop first
23
- elem = q.pop
24
- break if stop.equal?(elem)
16
+ elem = queue.pop
17
+ if SlowEnumeratorTools::Util::STOP_OK.equal?(elem)
18
+ break
19
+ elsif SlowEnumeratorTools::Util::STOP_ERR.equal?(elem)
20
+ raise queue.pop
21
+ end
25
22
  res << elem
26
23
 
27
24
  loop do
28
25
  # pop remaining
29
26
  begin
30
- elem = q.pop(true)
27
+ elem = queue.pop(true)
31
28
  rescue ThreadError
32
29
  break
33
30
  end
34
- if stop.equal?(elem)
31
+ if SlowEnumeratorTools::Util::STOP_OK.equal?(elem)
35
32
  ended = true
36
33
  break
34
+ elsif SlowEnumeratorTools::Util::STOP_ERR.equal?(elem)
35
+ raise queue.pop
37
36
  end
38
37
  res << elem
39
38
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlowEnumeratorTools
4
+ module Bufferer
5
+ def self.buffer(enum, size)
6
+ queue = SizedQueue.new(size)
7
+ thread = SlowEnumeratorTools::Util.gen_collector_thread(enum, queue)
8
+ gen_enumerator(queue, thread)
9
+ end
10
+
11
+ def self.gen_enumerator(queue, collector_thread)
12
+ Enumerator.new do |y|
13
+ loop do
14
+ elem = queue.pop
15
+
16
+ if SlowEnumeratorTools::Util::STOP_OK.equal?(elem)
17
+ break
18
+ elsif SlowEnumeratorTools::Util::STOP_ERR.equal?(elem)
19
+ raise queue.pop
20
+ end
21
+
22
+ y << elem
23
+ end
24
+ collector_thread.join
25
+ end.lazy
26
+ end
27
+ end
28
+ end
@@ -11,8 +11,6 @@ module SlowEnumeratorTools
11
11
  end
12
12
 
13
13
  class Iterator
14
- DONE = Object.new
15
-
16
14
  def initialize(enums)
17
15
  @enums = enums
18
16
  @q = SizedQueue.new(5)
@@ -23,9 +21,11 @@ module SlowEnumeratorTools
23
21
  raise StopIteration if @done
24
22
 
25
23
  nxt = @q.pop
26
- if DONE.equal?(nxt)
24
+ if SlowEnumeratorTools::Util::STOP_OK.equal?(nxt)
27
25
  @done = true
28
26
  raise StopIteration
27
+ elsif SlowEnumeratorTools::Util::STOP_ERR.equal?(nxt)
28
+ raise @q.pop
29
29
  else
30
30
  nxt
31
31
  end
@@ -34,24 +34,22 @@ module SlowEnumeratorTools
34
34
  def start
35
35
  threads = @enums.map { |enum| spawn_empty_into(enum, @q) }
36
36
 
37
- spawn do
37
+ Thread.new do
38
38
  threads.each(&:join)
39
- @q << DONE
39
+ @q << SlowEnumeratorTools::Util::STOP_OK
40
40
  end
41
41
  end
42
42
 
43
43
  protected
44
44
 
45
45
  def spawn_empty_into(enum, queue)
46
- spawn do
47
- enum.each { |e| queue << e }
48
- end
49
- end
50
-
51
- def spawn
52
46
  Thread.new do
53
- Thread.current.abort_on_exception = true
54
- yield
47
+ begin
48
+ enum.each { |e| queue << e }
49
+ rescue StandardError => e
50
+ queue << SlowEnumeratorTools::Util::STOP_ERR
51
+ queue << e
52
+ end
55
53
  end
56
54
  end
57
55
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlowEnumeratorTools
4
+ module Util
5
+ STOP_OK = Object.new
6
+ STOP_ERR = Object.new
7
+
8
+ def self.gen_collector_thread(enum, queue)
9
+ Thread.new do
10
+ begin
11
+ enum.each { |e| queue << e }
12
+ queue << STOP_OK
13
+ rescue StandardError => e
14
+ queue << STOP_ERR
15
+ queue << e
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SlowEnumeratorTools
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -34,4 +34,87 @@ describe SlowEnumeratorTools::Batcher do
34
34
  sleep 0.45
35
35
  expect(subject.next).to eq([0, 1, 2])
36
36
  end
37
+
38
+ context 'empty enumerable' do
39
+ let(:wrapped) do
40
+ Enumerator.new do |y|
41
+ end
42
+ end
43
+
44
+ it 'returns nothing' do
45
+ expect { subject.next }.to raise_error(StopIteration)
46
+ end
47
+ end
48
+
49
+ context 'instant-erroring enumerable' do
50
+ let(:wrapped) do
51
+ Enumerator.new do |_y|
52
+ raise 'boom'
53
+ end
54
+ end
55
+
56
+ it 'returns nothing' do
57
+ expect { subject.next }.to raise_error(RuntimeError, 'boom')
58
+ end
59
+ end
60
+
61
+ context 'error in taken elements' do
62
+ let(:wrapped) do
63
+ Enumerator.new do |y|
64
+ y << 1
65
+ y << 2
66
+ y << 3
67
+ raise 'boom'
68
+ end
69
+ end
70
+
71
+ it 'does not raise right away' do
72
+ subject
73
+ sleep 0.01
74
+ subject
75
+ end
76
+
77
+ it 'does not raise when only taken' do
78
+ subject
79
+ sleep 0.01
80
+ subject.take(1)
81
+ end
82
+
83
+ it 'raises when evaluated' do
84
+ subject
85
+ sleep 0.01
86
+ expect { subject.take(1).first }
87
+ .to raise_error(RuntimeError, 'boom')
88
+ end
89
+ end
90
+
91
+ context 'error past taken elements' do
92
+ let(:wrapped) do
93
+ Enumerator.new do |y|
94
+ y << 1
95
+ y << 2
96
+ y << 3
97
+ sleep 0.1
98
+ raise 'boom'
99
+ end
100
+ end
101
+
102
+ it 'does not raise right away' do
103
+ subject
104
+ sleep 0.01
105
+ subject
106
+ end
107
+
108
+ it 'does not raise when only taken' do
109
+ subject
110
+ sleep 0.01
111
+ subject.take(1)
112
+ end
113
+
114
+ it 'does not raise when evaluated' do
115
+ subject
116
+ sleep 0.01
117
+ expect(subject.take(1).first).to eq([1, 2, 3])
118
+ end
119
+ end
37
120
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe SlowEnumeratorTools::Bufferer do
4
+ describe '.new + #call' do
5
+ subject { described_class.buffer(enum, size) }
6
+
7
+ let(:enum) { [] }
8
+ let(:size) { 10 }
9
+
10
+ context 'empty array' do
11
+ example do
12
+ expect(subject.to_a).to eq([])
13
+ end
14
+ end
15
+
16
+ context 'small array' do
17
+ let(:enum) { [1, 2, 3] }
18
+
19
+ example do
20
+ expect(subject.to_a).to eq([1, 2, 3])
21
+ end
22
+ end
23
+
24
+ context 'infinite array' do
25
+ let(:enum) { (1..(1.0 / 0)) }
26
+
27
+ example do
28
+ expect(subject.take(3).to_a).to eq([1, 2, 3])
29
+ end
30
+ end
31
+
32
+ context 'error in taken elements' do
33
+ let(:enum) do
34
+ Enumerator.new do |y|
35
+ y << 1
36
+ y << 2
37
+ raise 'boom'
38
+ end
39
+ end
40
+
41
+ it 'does not raise right away' do
42
+ subject
43
+ end
44
+
45
+ it 'does not raise when only taken' do
46
+ subject.take(3)
47
+ end
48
+
49
+ it 'raises when evaluated' do
50
+ expect { subject.take(3).to_a }
51
+ .to raise_error(RuntimeError, 'boom')
52
+ end
53
+ end
54
+
55
+ context 'error past taken elements' do
56
+ let(:enum) do
57
+ Enumerator.new do |y|
58
+ y << 1
59
+ y << 2
60
+ y << 3
61
+ raise 'boom'
62
+ end
63
+ end
64
+
65
+ it 'does not raise right away' do
66
+ subject
67
+ end
68
+
69
+ it 'does not raise when only taken' do
70
+ subject.take(3)
71
+ end
72
+
73
+ it 'does not raise when evaluated' do
74
+ expect(subject.take(3).to_a).to eq([1, 2, 3])
75
+ end
76
+ end
77
+
78
+ context 'slow source' do
79
+ let(:enum) do
80
+ Enumerator.new do |y|
81
+ 20.times do |i|
82
+ y << i
83
+ sleep 0.1
84
+ end
85
+ end
86
+ end
87
+
88
+ it 'takes a while to take elements when unbuffered' do
89
+ expect { subject.take(5).to_a }
90
+ .to finish_in(0.5, 0.1)
91
+ end
92
+
93
+ it 'takes no time to take elements when buffered' do
94
+ subject
95
+ sleep 0.5
96
+ expect { subject.take(5).to_a }
97
+ .to finish_in(0.0, 0.1)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -67,4 +67,58 @@ describe SlowEnumeratorTools::Merger do
67
67
  let(:enums) { [[1, 2], [3, 4]] }
68
68
  it { is_expected.to match_array([1, 2, 3, 4]) }
69
69
  end
70
+
71
+ context 'error in taken elements' do
72
+ subject { described_class.merge(enums) }
73
+
74
+ let(:enum) do
75
+ Enumerator.new do |y|
76
+ y << 1
77
+ y << 2
78
+ raise 'boom'
79
+ end.lazy
80
+ end
81
+
82
+ let(:enums) { [enum] }
83
+
84
+ it 'does not raise right away' do
85
+ subject
86
+ end
87
+
88
+ it 'does not raise when only taken' do
89
+ subject.take(3)
90
+ end
91
+
92
+ it 'raises when evaluated' do
93
+ expect { subject.take(3).to_a }
94
+ .to raise_error(RuntimeError, 'boom')
95
+ end
96
+ end
97
+
98
+ context 'error past taken elements' do
99
+ subject { described_class.merge(enums) }
100
+
101
+ let(:enum) do
102
+ Enumerator.new do |y|
103
+ y << 1
104
+ y << 2
105
+ y << 3
106
+ raise 'boom'
107
+ end.lazy
108
+ end
109
+
110
+ let(:enums) { [enum] }
111
+
112
+ it 'does not raise right away' do
113
+ subject
114
+ end
115
+
116
+ it 'does not raise when only taken' do
117
+ subject.take(3)
118
+ end
119
+
120
+ it 'does not raise when evaluated' do
121
+ expect(subject.take(3).to_a).to eq([1, 2, 3])
122
+ end
123
+ end
70
124
  end
@@ -14,4 +14,9 @@ describe SlowEnumeratorTools do
14
14
  expect(SlowEnumeratorTools.batch([1, 2]).to_a)
15
15
  .to match_array([[1, 2]])
16
16
  end
17
+
18
+ it 'supports .buffer shorthand' do
19
+ expect(SlowEnumeratorTools.buffer([1, 2], 5).to_a)
20
+ .to match_array([1, 2])
21
+ end
17
22
  end
@@ -15,3 +15,24 @@ RSpec.configure do |c|
15
15
  format: '%c/%C |<%b>%i| %p%%',
16
16
  }
17
17
  end
18
+
19
+ RSpec::Matchers.define :finish_in do |expected, delta|
20
+ supports_block_expectations
21
+
22
+ match do |actual|
23
+ before = Time.now
24
+ actual.call
25
+ after = Time.now
26
+ @actual_duration = after - before
27
+ range = (expected - delta..expected + delta)
28
+ range.include?(@actual_duration)
29
+ end
30
+
31
+ failure_message do |_actual|
32
+ "expected that proc would finish in #{expected}s (±#{delta}s), but took #{format '%0.1fs', @actual_duration}"
33
+ end
34
+
35
+ failure_message_when_negated do |_actual|
36
+ "expected that proc would not finish in #{expected}s (±#{delta}s), but took #{format '%0.1fs', @actual_duration}"
37
+ end
38
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slow_enumerator_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Defreyne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-21 00:00:00.000000000 Z
11
+ date: 2017-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -43,11 +43,14 @@ files:
43
43
  - Rakefile
44
44
  - lib/slow_enumerator_tools.rb
45
45
  - lib/slow_enumerator_tools/batcher.rb
46
+ - lib/slow_enumerator_tools/bufferer.rb
46
47
  - lib/slow_enumerator_tools/merger.rb
48
+ - lib/slow_enumerator_tools/util.rb
47
49
  - lib/slow_enumerator_tools/version.rb
48
50
  - scripts/release
49
51
  - slow_enumerator_tools.gemspec
50
52
  - spec/batcher_spec.rb
53
+ - spec/bufferer_spec.rb
51
54
  - spec/merger_spec.rb
52
55
  - spec/slow_enumerator_tools_spec.rb
53
56
  - spec/spec_helper.rb
@@ -71,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
74
  version: '0'
72
75
  requirements: []
73
76
  rubyforge_project:
74
- rubygems_version: 2.6.14
77
+ rubygems_version: 2.7.1
75
78
  signing_key:
76
79
  specification_version: 4
77
80
  summary: provides tools for transforming Ruby enumerators that produce data slowly