conveyor 0.1.4 → 0.2.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.tar.gz.sig +0 -0
- data/History.txt +9 -0
- data/Manifest.txt +2 -1
- data/Rakefile +1 -1
- data/bin/conveyor +9 -5
- data/bin/conveyor-upgrade +10 -0
- data/lib/conveyor.rb +1 -1
- data/lib/conveyor/base_channel.rb +132 -50
- data/lib/conveyor/channel.rb +65 -40
- data/lib/conveyor/client.rb +22 -21
- data/lib/conveyor/server.rb +130 -141
- data/lib/conveyor/upgrader.rb +119 -0
- data/test/test_channel.rb +24 -11
- data/test/test_replicated_channel.rb +12 -3
- data/test/test_server.rb +104 -74
- metadata +6 -4
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/History.txt
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
== 0.2.0 / 2008-02-26
|
2
|
+
|
3
|
+
* switched from using Mongrel to Thin. this adds a dependency on thin, which depends on Event Machine
|
4
|
+
* GZIPed storage. Entries over an arbitrary size (>=256 bytes) are compressed before written down. Since this constitutes a change in file format, the script conveyor-upgrade should be run on all date before running the new version.
|
5
|
+
* Channels can now be deleted. This is very unsafe and only available when running with the --unsafe switch.
|
6
|
+
* Channels are auto-created the first time you try and post a message to them.
|
7
|
+
* Only channels with valid names are opened by the server.
|
8
|
+
* The client API was refactored to be more like the Channel API and Channel-specific.
|
9
|
+
|
1
10
|
== 0.1.4 / 2008-02-19
|
2
11
|
* support for getting multiple items at once
|
3
12
|
* rewinding implemented for group iterators
|
data/Manifest.txt
CHANGED
@@ -4,6 +4,7 @@ Manifest.txt
|
|
4
4
|
README.txt
|
5
5
|
Rakefile
|
6
6
|
bin/conveyor
|
7
|
+
bin/conveyor-upgrade
|
7
8
|
docs/file-formats.mkd
|
8
9
|
docs/protocol.mkd
|
9
10
|
lib/conveyor.rb
|
@@ -11,10 +12,10 @@ lib/conveyor/base_channel.rb
|
|
11
12
|
lib/conveyor/channel.rb
|
12
13
|
lib/conveyor/client.rb
|
13
14
|
lib/conveyor/server.rb
|
15
|
+
lib/conveyor/upgrader.rb
|
14
16
|
lib/priority_queue.rb
|
15
17
|
test/rand.rb
|
16
18
|
test/test_channel.rb
|
17
19
|
test/test_feeder-ng.rb
|
18
20
|
test/test_priority_queue.rb
|
19
|
-
test/test_replicated_channel.rb
|
20
21
|
test/test_server.rb
|
data/Rakefile
CHANGED
data/bin/conveyor
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'rubygems'
|
5
5
|
require 'daemons/daemonize'
|
6
|
+
require 'thin'
|
6
7
|
|
7
8
|
options = {:port => 8011, :daemonize => false}
|
8
9
|
opts = OptionParser.new do |opts|
|
@@ -22,6 +23,10 @@ opts = OptionParser.new do |opts|
|
|
22
23
|
opts.on("-d", "Daemonize.") do |d|
|
23
24
|
options[:daemonize] = d
|
24
25
|
end
|
26
|
+
|
27
|
+
opts.on("--unsafe", "Unsafe Mode.") do |d|
|
28
|
+
options[:unsafe] = d
|
29
|
+
end
|
25
30
|
end
|
26
31
|
|
27
32
|
opts.parse!
|
@@ -39,10 +44,9 @@ if options[:daemonize]
|
|
39
44
|
Daemonize.daemonize
|
40
45
|
end
|
41
46
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
Thin::Server.start('0.0.0.0', options[:port]) do
|
48
|
+
map '/channels' do
|
49
|
+
run Conveyor::App.new(ARGV[0], :log_directory => options[:log_directory], :unsafe_mode => options[:unsafe])
|
50
|
+
end
|
46
51
|
end
|
47
52
|
|
48
|
-
server.run.join
|
data/lib/conveyor.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
require 'rubygems'
|
3
3
|
require 'time'
|
4
|
-
require '
|
4
|
+
require 'stringio'
|
5
|
+
require 'zlib'
|
6
|
+
require 'json'
|
5
7
|
|
6
8
|
module Conveyor
|
7
9
|
# BaseChannel
|
@@ -11,43 +13,37 @@ module Conveyor
|
|
11
13
|
|
12
14
|
NAME_PATTERN = %r{\A[a-zA-Z\-0-9\_]+\Z}
|
13
15
|
BUCKET_SIZE = 100_000
|
16
|
+
FORMAT_VERSION = 1
|
17
|
+
|
18
|
+
module Flags
|
19
|
+
GZIP = 1
|
20
|
+
end
|
14
21
|
|
15
22
|
def initialize directory
|
16
|
-
@directory
|
17
|
-
@data_files
|
18
|
-
@
|
19
|
-
@
|
23
|
+
@directory = directory
|
24
|
+
@data_files = []
|
25
|
+
@file_mutexes = []
|
26
|
+
@index = []
|
27
|
+
@iterator = 1
|
28
|
+
@id_lock = Mutex.new
|
20
29
|
|
21
30
|
if File.exists?(@directory)
|
22
31
|
if !File.directory?(@directory)
|
23
32
|
raise "#{@directory} is not a directory"
|
24
33
|
end
|
34
|
+
load_channel
|
25
35
|
else
|
26
36
|
Dir.mkdir(@directory)
|
37
|
+
setup_channel
|
27
38
|
end
|
28
39
|
|
29
|
-
|
30
|
-
|
31
|
-
if File.exists?(index_path) && File.size(index_path) > 0
|
32
|
-
@index_file = File.open(index_path, 'r+')
|
33
|
-
|
34
|
-
@index_file.each_line do |line|
|
35
|
-
@index << parse_headers(line.strip, true)
|
36
|
-
@last_id = @index.last[:id]
|
37
|
-
end
|
38
|
-
@index_file.seek(0, IO::SEEK_END)
|
39
|
-
else
|
40
|
-
@index_file = File.open(index_path, 'a')
|
41
|
-
@last_id = 0
|
42
|
-
end
|
43
|
-
@index_file.sync = true
|
44
|
-
|
40
|
+
@index_file.sync = true
|
45
41
|
end
|
46
|
-
|
42
|
+
|
47
43
|
def inspect
|
48
44
|
"<#{self.class} dir:'#{@directory.to_s}' last_id:#{@last_id} iterator:#{@iterator}>"
|
49
45
|
end
|
50
|
-
|
46
|
+
|
51
47
|
def pick_bucket i
|
52
48
|
(i / BUCKET_SIZE).to_i
|
53
49
|
end
|
@@ -56,63 +52,149 @@ module Conveyor
|
|
56
52
|
unless @data_files[i]
|
57
53
|
@data_files[i] = File.open(File.join(@directory, i.to_s), 'a+')
|
58
54
|
@data_files[i].sync = true
|
55
|
+
@file_mutexes[i] = Mutex.new
|
56
|
+
end
|
57
|
+
@file_mutexes[i].synchronize do
|
58
|
+
yield @data_files[i]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def id_lock
|
63
|
+
@id_lock.synchronize do
|
64
|
+
yield
|
59
65
|
end
|
60
|
-
yield @data_files[i]
|
61
66
|
end
|
62
67
|
|
63
68
|
def commit data, time = nil
|
64
|
-
|
69
|
+
l = nil
|
70
|
+
gzip = data.length >= 256
|
71
|
+
if gzip
|
72
|
+
compressed_data = StringIO.new
|
73
|
+
g = Zlib::GzipWriter.new(compressed_data)
|
74
|
+
g << data
|
75
|
+
g.finish
|
76
|
+
compressed_data.rewind
|
77
|
+
compressed_data = compressed_data.read
|
78
|
+
l = compressed_data.length
|
79
|
+
else
|
80
|
+
l = data.length
|
81
|
+
end
|
82
|
+
|
83
|
+
h = Digest::MD5.hexdigest(data)
|
84
|
+
|
85
|
+
id_lock do
|
65
86
|
i = @last_id + 1
|
66
87
|
t = time || Time.now
|
67
|
-
l = data.length
|
68
|
-
h = Digest::MD5.hexdigest(data)
|
69
88
|
b = pick_bucket(i)
|
89
|
+
flags = 0
|
90
|
+
flags = flags | Flags::GZIP if gzip
|
70
91
|
header, o = nil
|
71
92
|
bucket_file(b) do |f|
|
72
93
|
f.seek(0, IO::SEEK_END)
|
73
94
|
o = f.pos
|
74
|
-
header = "#{i} #{t.
|
75
|
-
f.write("#{header}\n"
|
95
|
+
header = "#{i.to_s(36)} #{t.to_i.to_s(36)} #{o.to_s(36)} #{l.to_s(36)} #{h} #{flags.to_s(36)}"
|
96
|
+
f.write("#{header}\n")
|
97
|
+
f.write((gzip ? compressed_data : data))
|
98
|
+
f.write("\n")
|
76
99
|
end
|
77
100
|
|
78
101
|
@last_id = i
|
79
|
-
@index_file.write "#{header} #{b}\n"
|
102
|
+
@index_file.write "#{header} #{b.to_s(36)}\n"
|
80
103
|
@index << {:id => i, :time => t, :offset => o, :length => l, :hash => h, :file => b}
|
81
104
|
i
|
82
105
|
end
|
83
106
|
end
|
84
107
|
|
85
|
-
def get id
|
86
|
-
return nil unless id <= @last_id
|
87
|
-
i = @index
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
108
|
+
def get id, stream = false
|
109
|
+
return nil unless id <= @last_id && id > 0
|
110
|
+
i = @index[id-1]
|
111
|
+
headers, content, compressed_content, g = nil
|
112
|
+
bucket_file(i[:file]) do |f|
|
113
|
+
f.seek i[:offset]
|
114
|
+
headers = parse_headers(f.readline.strip)
|
115
|
+
compressed_content = f.read(i[:length])
|
116
|
+
end
|
117
|
+
io = StringIO.new(compressed_content)
|
118
|
+
if (headers[:flags] & Flags::GZIP) != 0
|
119
|
+
g = Zlib::GzipReader.new(io)
|
120
|
+
else
|
121
|
+
g = io
|
122
|
+
end
|
123
|
+
if stream
|
124
|
+
[headers, g]
|
125
|
+
else
|
126
|
+
[headers, g.read]
|
95
127
|
end
|
96
|
-
[parse_headers(header), content]
|
97
128
|
end
|
98
129
|
|
99
|
-
def parse_headers str, index_file=false
|
100
|
-
pattern = '\A(\d+) (\d
|
130
|
+
def self.parse_headers str, index_file=false
|
131
|
+
pattern = '\A([a-z\d]+) ([a-z\d]+) ([a-z\d]+) ([a-z\d]+) ([a-f0-9]+) ([a-z\d]+)'
|
101
132
|
pattern += ' (\d+)' if index_file
|
102
133
|
pattern += '\Z'
|
103
134
|
m = str.match(Regexp.new(pattern))
|
104
135
|
{
|
105
|
-
:id
|
106
|
-
:time
|
107
|
-
:offset => m.captures[2].to_i,
|
108
|
-
:length => m.captures[3].to_i,
|
109
|
-
:hash
|
110
|
-
:
|
111
|
-
|
136
|
+
:id => m.captures[0].to_i(36),
|
137
|
+
:time => m.captures[1].to_i(36),
|
138
|
+
:offset => m.captures[2].to_i(36),
|
139
|
+
:length => m.captures[3].to_i(36),
|
140
|
+
:hash => m.captures[4],
|
141
|
+
:flags => m.captures[5].to_i(36),
|
142
|
+
:file => (index_file ? m.captures[6].to_i(36) : nil)
|
143
|
+
}
|
112
144
|
end
|
113
145
|
|
146
|
+
def parse_headers str, index_file=false
|
147
|
+
self.class.parse_headers str, index_file
|
148
|
+
end
|
149
|
+
|
114
150
|
def self.valid_channel_name? name
|
115
151
|
!!name.match(NAME_PATTERN)
|
116
152
|
end
|
153
|
+
|
154
|
+
def delete!
|
155
|
+
FileUtils.rm_r(@directory)
|
156
|
+
@index = []
|
157
|
+
@data_files =[]
|
158
|
+
@last_id = 0
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
def setup_channel
|
164
|
+
@index_file = File.open(index_path, 'a')
|
165
|
+
@last_id = 0
|
166
|
+
@version = FORMAT_VERSION
|
167
|
+
File.open(version_path, 'w+'){|f| f.write(@version.to_s)}
|
168
|
+
end
|
169
|
+
|
170
|
+
def load_channel
|
171
|
+
if File.exists?(version_path) && File.size(version_path) > 0
|
172
|
+
@version = File.open(version_path).read.to_i
|
173
|
+
else
|
174
|
+
@version = 0
|
175
|
+
end
|
176
|
+
|
177
|
+
if @version != FORMAT_VERSION
|
178
|
+
raise "Format versions don't match. Try upgrading."
|
179
|
+
end
|
180
|
+
|
181
|
+
@index_file = File.open(index_path, 'r+')
|
182
|
+
|
183
|
+
@index_file.each_line do |line|
|
184
|
+
@index << parse_headers(line.strip, true)
|
185
|
+
@last_id = @index.last[:id]
|
186
|
+
end
|
187
|
+
@index_file.seek(0, IO::SEEK_END)
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
def index_path
|
192
|
+
File.join(@directory, 'index')
|
193
|
+
end
|
194
|
+
|
195
|
+
def version_path
|
196
|
+
File.join(@directory, 'version')
|
197
|
+
end
|
198
|
+
|
117
199
|
end
|
118
200
|
end
|
data/lib/conveyor/channel.rb
CHANGED
@@ -8,33 +8,14 @@ module Conveyor
|
|
8
8
|
|
9
9
|
# If +directory+ doesn't already exist, it will be created during initialization.
|
10
10
|
def initialize directory
|
11
|
-
@group_iterators
|
12
|
-
@group_iterators_files
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
iterator_path = File.join(@directory, 'iterator')
|
11
|
+
@group_iterators = {}
|
12
|
+
@group_iterators_files = {}
|
13
|
+
@iterator_lock = Mutex.new
|
14
|
+
@group_iterator_locks = Hash.new{|k,v| Mutex.new }
|
17
15
|
|
18
|
-
|
19
|
-
@iterator_file = File.open(iterator_path, 'r+')
|
20
|
-
@iterator_file.each_line do |line|
|
21
|
-
@iterator = line.to_i
|
22
|
-
end
|
23
|
-
@iterator_file.seek(0, IO::SEEK_END)
|
24
|
-
else
|
25
|
-
@iterator_file = File.open(iterator_path, 'a')
|
26
|
-
end
|
27
|
-
@iterator_file.sync = true
|
16
|
+
super(directory)
|
28
17
|
|
29
|
-
|
30
|
-
g = i.split(%r{/}).last.match(%r{iterator-(.*)}).captures[0]
|
31
|
-
@group_iterators_files[g] = File.open(i, 'r+')
|
32
|
-
@group_iterators[g] = 1
|
33
|
-
@group_iterators_files[g].each_line do |line|
|
34
|
-
@group_iterators[g] = line.to_i
|
35
|
-
end
|
36
|
-
@group_iterators_files[g].seek(0, IO::SEEK_END)
|
37
|
-
end
|
18
|
+
@iterator_file.sync = true
|
38
19
|
end
|
39
20
|
|
40
21
|
# Add data to the channel.
|
@@ -45,11 +26,11 @@ module Conveyor
|
|
45
26
|
# Returns the next item from the global (non-group) iterator.
|
46
27
|
def get_next
|
47
28
|
r = nil
|
48
|
-
|
29
|
+
iterator_lock do
|
49
30
|
if @iterator <= @last_id
|
50
31
|
r = get(@iterator)
|
51
32
|
@iterator += 1
|
52
|
-
@iterator_file.write("#{@iterator}\n")
|
33
|
+
@iterator_file.write("#{@iterator.to_s(36)}\n")
|
53
34
|
r
|
54
35
|
else
|
55
36
|
nil
|
@@ -60,13 +41,13 @@ module Conveyor
|
|
60
41
|
# Returns the next item for +group+. If +group+ hasn't been seen before, the first item is returned.
|
61
42
|
def get_next_by_group group
|
62
43
|
r = nil
|
63
|
-
|
44
|
+
group_iterator_lock(group) do
|
64
45
|
@group_iterators[group] = 1 unless @group_iterators.key?(group)
|
65
46
|
if @iterator <= @last_id
|
66
47
|
r = get(@group_iterators[group])
|
67
48
|
@group_iterators[group] += 1
|
68
49
|
group_iterators_file(group) do |f|
|
69
|
-
f.write("#{@group_iterators[group]}\n")
|
50
|
+
f.write("#{@group_iterators[group].to_s(36)}\n")
|
70
51
|
end
|
71
52
|
else
|
72
53
|
nil
|
@@ -77,11 +58,11 @@ module Conveyor
|
|
77
58
|
|
78
59
|
def get_next_n n
|
79
60
|
r = []
|
80
|
-
|
61
|
+
iterator_lock do
|
81
62
|
while r.length < n && @iterator <= @last_id
|
82
63
|
r << get(@iterator)
|
83
64
|
@iterator += 1
|
84
|
-
@iterator_file.write("#{@iterator}\n")
|
65
|
+
@iterator_file.write("#{@iterator.to_s(36)}\n")
|
85
66
|
r
|
86
67
|
end
|
87
68
|
end
|
@@ -90,13 +71,13 @@ module Conveyor
|
|
90
71
|
|
91
72
|
def get_next_n_by_group n, group
|
92
73
|
r = []
|
93
|
-
|
74
|
+
group_iterator_lock(group) do
|
94
75
|
@group_iterators[group] = 1 unless @group_iterators.key?(group)
|
95
76
|
while r.length < n && @group_iterators[group] < @last_id
|
96
77
|
r << get(@group_iterators[group])
|
97
78
|
@group_iterators[group] += 1
|
98
79
|
group_iterators_file(group) do |f|
|
99
|
-
f.write("#{@group_iterators[group]}\n")
|
80
|
+
f.write("#{@group_iterators[group].to_s(36)}\n")
|
100
81
|
end
|
101
82
|
end
|
102
83
|
end
|
@@ -119,23 +100,36 @@ module Conveyor
|
|
119
100
|
opts = opts.first
|
120
101
|
if opts.key?(:id)
|
121
102
|
if opts.key?(:group)
|
122
|
-
|
103
|
+
group_iterator_lock(opts[:group]) do
|
123
104
|
@group_iterators[opts[:group]] = opts[:id].to_i
|
124
105
|
group_iterators_file(opts[:group]) do |f|
|
125
|
-
f.write("#{@group_iterators[opts[:group]]}\n")
|
106
|
+
f.write("#{@group_iterators[opts[:group]].to_s(36)}\n")
|
126
107
|
end
|
127
108
|
end
|
128
109
|
else
|
129
|
-
|
110
|
+
iterator_lock do
|
130
111
|
@iterator = opts[:id].to_i
|
131
|
-
@iterator_file.write("#{@iterator}\n")
|
112
|
+
@iterator_file.write("#{@iterator.to_s(36)}\n")
|
132
113
|
end
|
133
114
|
end
|
134
115
|
end
|
135
116
|
end
|
136
|
-
|
117
|
+
|
118
|
+
|
137
119
|
private
|
138
|
-
|
120
|
+
|
121
|
+
def iterator_lock
|
122
|
+
@iterator_lock.synchronize do
|
123
|
+
yield
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def group_iterator_lock group
|
128
|
+
@group_iterator_locks[group].synchronize do
|
129
|
+
yield
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
139
133
|
def group_iterators_file group
|
140
134
|
unless @group_iterators_files[group]
|
141
135
|
@group_iterators_files[group] = File.open(File.join(@directory, 'iterator-' + group), 'a+')
|
@@ -143,6 +137,37 @@ module Conveyor
|
|
143
137
|
end
|
144
138
|
yield @group_iterators_files[group]
|
145
139
|
end
|
146
|
-
|
140
|
+
|
141
|
+
def load_channel
|
142
|
+
super
|
143
|
+
@iterator_file = File.open(iterator_path, 'r+')
|
144
|
+
@iterator_file.each_line do |line|
|
145
|
+
@iterator = line.to_i(36)
|
146
|
+
end
|
147
|
+
@iterator_file.seek(0, IO::SEEK_END)
|
148
|
+
|
149
|
+
Dir.glob(File.join(@directory, 'iterator-*')) do |i|
|
150
|
+
g = i.split(%r{/}).last.match(%r{iterator-(.*)}).captures[0]
|
151
|
+
@group_iterators_files[g] = File.open(i, 'r+')
|
152
|
+
@group_iterators[g] = 1
|
153
|
+
@group_iterators_files[g].each_line do |line|
|
154
|
+
@group_iterators[g] = line.to_i(36)
|
155
|
+
end
|
156
|
+
@group_iterators_files[g].seek(0, IO::SEEK_END)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def setup_channel
|
161
|
+
super
|
162
|
+
@iterator_file = File.open(iterator_path, 'a')
|
163
|
+
end
|
164
|
+
|
165
|
+
def iterator_path
|
166
|
+
File.join(@directory, 'iterator')
|
167
|
+
end
|
168
|
+
|
169
|
+
def version_path
|
170
|
+
File.join(@directory, 'version')
|
171
|
+
end
|
147
172
|
end
|
148
173
|
end
|