burt-delay_queue 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.
@@ -0,0 +1,2 @@
1
+ /.rvmrc
2
+ /pkg
@@ -0,0 +1,21 @@
1
+ $: << File.expand_path('../lib', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'burt-delay_queue'
5
+ s.version = '1.1.0'
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ['Theo Hultberg']
8
+ s.email = ['theo@burtcorp.com']
9
+ s.homepage = 'https://github.com/iconara/delay_queue'
10
+ s.summary = 'A TTL based priority queue'
11
+ s.description = 'Delay queue keeps it\'s elements ordered by a timestamp, popping off the items with the lowest timestamp first'
12
+
13
+ s.rubyforge_project = 'delay_queue'
14
+
15
+ s.add_development_dependency 'rspec', '~> 2.5.0'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ # s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ # s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ['lib']
21
+ end
@@ -0,0 +1,157 @@
1
+ # encoding: utf-8
2
+
3
+ if RUBY_PLATFORM == 'java'
4
+ require 'java'
5
+
6
+ class DelayQueue
7
+ java_import 'java.util.TreeMap'
8
+ java_import 'java.util.HashSet'
9
+
10
+ def initialize(clock=Time)
11
+ @clock = clock
12
+ @timestamp_to_elements = TreeMap.new
13
+ @element_to_timestamp = {}
14
+ end
15
+
16
+ def put(element, timestamp=Time.now.to_i, options={})
17
+ existing_timestamp = @element_to_timestamp[element]
18
+ if !existing_timestamp || existing_timestamp < timestamp || options[:force]
19
+ remove(element) if existing_timestamp
20
+ elements = @timestamp_to_elements.get(timestamp)
21
+ elements ||= HashSet.new
22
+ elements.add(element)
23
+ @timestamp_to_elements.put(timestamp, elements)
24
+ @element_to_timestamp[element] = timestamp
25
+ end
26
+ end
27
+
28
+ def remove(element)
29
+ timestamp = @element_to_timestamp.delete(element)
30
+ if timestamp
31
+ elements = @timestamp_to_elements.get(timestamp)
32
+ elements.remove(element)
33
+ @timestamp_to_elements.remove(timestamp) if elements.empty?
34
+ end
35
+ end
36
+
37
+ def pop(n=1)
38
+ popped_elements = []
39
+ loop do
40
+ entry = @timestamp_to_elements.first_entry
41
+ break unless entry
42
+ break if entry.key > @clock.now.to_i
43
+ elements = entry.value
44
+ iterator = elements.iterator
45
+ while iterator.has_next && popped_elements.size < n
46
+ element = iterator.next
47
+ @element_to_timestamp.delete(element)
48
+ iterator.remove
49
+ popped_elements << element
50
+ end
51
+ break unless elements.empty?
52
+ @timestamp_to_elements.delete(entry.key)
53
+ end
54
+ if n == 1
55
+ popped_elements.first
56
+ else
57
+ popped_elements
58
+ end
59
+ end
60
+
61
+ def pop_all
62
+ popped_elements = []
63
+ cutoff = @timestamp_to_elements.floor_key(@clock.now.to_i)
64
+ if cutoff
65
+ loop do
66
+ entry = @timestamp_to_elements.poll_first_entry
67
+ elements = entry.value
68
+ elements.each do |element|
69
+ @element_to_timestamp.delete(element)
70
+ popped_elements << element
71
+ end
72
+ break if entry.key == cutoff
73
+ end
74
+ end
75
+ popped_elements
76
+ end
77
+
78
+ def include?(element)
79
+ @element_to_timestamp.key?(element)
80
+ end
81
+
82
+ def to_h
83
+ @element_to_timestamp.dup
84
+ end
85
+
86
+ def size
87
+ @element_to_timestamp.size
88
+ end
89
+ end
90
+ else
91
+ require 'set'
92
+
93
+ class DelayQueue
94
+ def initialize(clock=Time)
95
+ @clock = clock
96
+ @elements = {}
97
+ @timestamps = SortedSet.new
98
+ @reverse_elements = Hash.new { |h, k| h[k] = Set.new }
99
+ end
100
+
101
+ def put(element, timestamp=Time.now.to_i, options={})
102
+ if @elements[element]
103
+ return unless options[:force] || timestamp > @elements[element]
104
+ remove(element)
105
+ end
106
+ @elements[element] = timestamp
107
+ @reverse_elements[timestamp] << element
108
+ @timestamps << timestamp
109
+ end
110
+
111
+ def remove(element)
112
+ timestamp = @elements.delete(element)
113
+ return unless timestamp
114
+ @reverse_elements[timestamp].delete(element)
115
+ if @reverse_elements[timestamp].empty?
116
+ @reverse_elements.delete(timestamp)
117
+ @timestamps.delete(timestamp)
118
+ end
119
+ end
120
+
121
+ def pop(n=1)
122
+ elements = peek_all.take(n)
123
+ elements.each { |e| remove(e) }
124
+ if n == 1
125
+ elements.first
126
+ else
127
+ elements
128
+ end
129
+ end
130
+
131
+ def pop_all
132
+ elements = peek_all
133
+ elements.each { |e| remove(e) }
134
+ elements
135
+ end
136
+
137
+ def include?(element)
138
+ @elements.key?(element)
139
+ end
140
+
141
+ def to_h
142
+ @elements.dup
143
+ end
144
+
145
+ def size
146
+ @elements.size
147
+ end
148
+
149
+ private
150
+
151
+ def peek_all
152
+ now = @clock.now.to_i
153
+ expired_timestamps = @timestamps.take_while { |ts| ts <= now }
154
+ expired_timestamps.flat_map { |ts| @reverse_elements[ts].to_a }
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,117 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'spec_helper'
4
+
5
+
6
+ class Clock
7
+ attr_accessor :now
8
+ end
9
+
10
+ describe DelayQueue do
11
+ before do
12
+ @clock = Clock.new
13
+ @q = DelayQueue.new(@clock)
14
+ end
15
+
16
+ describe '#put' do
17
+ it 'does not replace elements whose timestamp has been updated to an earlier time (by default)' do
18
+ @clock.now = 4
19
+ @q.put('blopp', 5)
20
+ @q.put('blipp', 4)
21
+ @q.put('blupp', 3)
22
+ @q.put('blopp', 1)
23
+ @q.pop.should == 'blupp'
24
+ end
25
+
26
+ it 'does not replace elements whose timestamp has been updated to an earlier time unless specifically asked to' do
27
+ @clock.now = 4
28
+ @q.put('blopp', 5)
29
+ @q.put('blipp', 4)
30
+ @q.put('blupp', 3)
31
+ @q.put('blopp', 1, :force => true)
32
+ @q.pop.should == 'blopp'
33
+ end
34
+ end
35
+
36
+ describe '#pop' do
37
+ it 'returns nil if no elements have expired' do
38
+ @clock.now = 1
39
+ @q.put('blipp', 4)
40
+ @q.pop.should be_nil
41
+ end
42
+
43
+ it 'returns the oldest expired element' do
44
+ @clock.now = 5
45
+ @q.put('blopp', 4)
46
+ @q.put('blipp', 3)
47
+ @q.pop.should == 'blipp'
48
+ end
49
+
50
+ it 'removes elements' do
51
+ @clock.now = 5
52
+ @q.put('blopp', 4)
53
+ @q.pop
54
+ @q.pop.should be_nil
55
+ end
56
+
57
+ it 'returns as many expired elements as you want, in age order' do
58
+ @clock.now = 5
59
+ @q.put('blopp', 5)
60
+ @q.put('blipp', 4)
61
+ @q.put('blupp', 3)
62
+ @q.pop(2).should == %w(blupp blipp)
63
+ end
64
+
65
+ it 'returns only as many elements as are available' do
66
+ @clock.now = 4
67
+ @q.put('blopp', 5)
68
+ @q.put('blipp', 4)
69
+ @q.put('blupp', 3)
70
+ @q.pop(10).should == %w(blupp blipp)
71
+ end
72
+
73
+ it 'does not return elements whose timestamp has been updated to a later time' do
74
+ @clock.now = 4
75
+ @q.put('blopp', 5)
76
+ @q.put('blipp', 4)
77
+ @q.put('blupp', 3)
78
+ @q.put('blipp', 10)
79
+ @q.pop(2).should == %w(blupp)
80
+ end
81
+ end
82
+
83
+ describe '#pop_all' do
84
+ it 'returns all expired elements' do
85
+ @clock.now = 3
86
+ @q.put('blopp', 1)
87
+ @q.put('blipp', 2)
88
+ @q.put('blupp', 3)
89
+ @q.put('blepp', 4)
90
+ @q.pop_all.should == %w(blopp blipp blupp)
91
+ end
92
+
93
+ it 'removes elements' do
94
+ @clock.now = 3
95
+ @q.put('blopp', 1)
96
+ @q.put('blipp', 2)
97
+ @q.put('blupp', 3)
98
+ @q.put('blepp', 4)
99
+ @q.pop_all.should == %w(blopp blipp blupp)
100
+ @q.pop_all.should == []
101
+ @clock.now = 4
102
+ @q.pop_all.should == ['blepp']
103
+ @q.pop_all.should == []
104
+ end
105
+ end
106
+
107
+ describe '#include?' do
108
+ it 'returns true if the element is in the queue' do
109
+ @q.put('x', 3)
110
+ @q.should include('x')
111
+ end
112
+
113
+ it 'returns false if the element is not in the queue' do
114
+ @q.should_not include('x')
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ $: << File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'delay_queue'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: burt-delay_queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Theo Hultberg
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-19 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70312679188040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.5.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70312679188040
25
+ description: Delay queue keeps it's elements ordered by a timestamp, popping off the
26
+ items with the lowest timestamp first
27
+ email:
28
+ - theo@burtcorp.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - delay_queue.gemspec
35
+ - lib/delay_queue.rb
36
+ - spec/delay_queue_spec.rb
37
+ - spec/spec_helper.rb
38
+ homepage: https://github.com/iconara/delay_queue
39
+ licenses: []
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project: delay_queue
58
+ rubygems_version: 1.8.15
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: A TTL based priority queue
62
+ test_files: []