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.
- data/.gitignore +2 -0
- data/delay_queue.gemspec +21 -0
- data/lib/delay_queue.rb +157 -0
- data/spec/delay_queue_spec.rb +117 -0
- data/spec/spec_helper.rb +5 -0
- metadata +62 -0
data/.gitignore
ADDED
data/delay_queue.gemspec
ADDED
@@ -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
|
data/lib/delay_queue.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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: []
|