slow_enumerator_tools 1.0.0 → 1.1.0

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