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/lib/conveyor/client.rb
CHANGED
@@ -2,53 +2,54 @@ require 'net/http'
|
|
2
2
|
|
3
3
|
module Conveyor
|
4
4
|
class Client
|
5
|
-
def initialize host, port = 8011
|
6
|
-
@host
|
7
|
-
@port
|
5
|
+
def initialize host, channel, port = 8011
|
6
|
+
@host = host
|
7
|
+
@port = port
|
8
|
+
@channel = channel
|
8
9
|
connect!
|
9
10
|
end
|
10
|
-
|
11
|
+
|
11
12
|
def connect!
|
12
13
|
@conn = Net::HTTP.start(@host, @port)
|
13
14
|
end
|
14
15
|
|
15
|
-
def create_channel
|
16
|
-
@conn.put("/channels/#{
|
16
|
+
def create_channel
|
17
|
+
@conn.put("/channels/#{@channel}", nil, {'Content-Type' => 'application/octet-stream'})
|
17
18
|
end
|
18
19
|
|
19
|
-
def post
|
20
|
-
@conn.post("/channels/#{
|
20
|
+
def post content
|
21
|
+
@conn.post("/channels/#{@channel}", content, {'Content-Type' => 'application/octet-stream', 'Date' => Time.now.gmtime.to_s})
|
21
22
|
end
|
22
23
|
|
23
|
-
def get
|
24
|
-
@conn.get("/channels/#{
|
24
|
+
def get id
|
25
|
+
@conn.get("/channels/#{@channel}/#{id}").body
|
25
26
|
end
|
26
27
|
|
27
|
-
def get_next
|
28
|
+
def get_next group=nil
|
28
29
|
if group
|
29
|
-
@conn.get("/channels/#{
|
30
|
+
@conn.get("/channels/#{@channel}?next&group=#{group}").body
|
30
31
|
else
|
31
|
-
@conn.get("/channels/#{
|
32
|
+
@conn.get("/channels/#{@channel}?next").body
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
|
-
def
|
36
|
-
JSON::parse(@conn.get("/channels/#{
|
36
|
+
def status
|
37
|
+
JSON::parse(@conn.get("/channels/#{@channel}").body)
|
37
38
|
end
|
38
39
|
|
39
|
-
def get_next_n
|
40
|
+
def get_next_n n = 10, group = nil
|
40
41
|
if group
|
41
|
-
JSON.parse(@conn.get("/channels/#{
|
42
|
+
JSON.parse(@conn.get("/channels/#{@channel}?next&n=#{n}&group=#{group}").body)
|
42
43
|
else
|
43
|
-
JSON.parse(@conn.get("/channels/#{
|
44
|
+
JSON.parse(@conn.get("/channels/#{@channel}?next&n=#{n}").body)
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
47
|
-
def rewind
|
48
|
+
def rewind id, group=nil
|
48
49
|
if group
|
49
|
-
@conn.post("/channels/#{
|
50
|
+
@conn.post("/channels/#{@channel}?rewind_id=#{id}&group=#{group}", nil)
|
50
51
|
else
|
51
|
-
@conn.post("/channels/#{
|
52
|
+
@conn.post("/channels/#{@channel}?rewind_id=#{id}", nil)
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|
data/lib/conveyor/server.rb
CHANGED
@@ -5,171 +5,160 @@ require 'fileutils'
|
|
5
5
|
require 'json'
|
6
6
|
require 'logger'
|
7
7
|
|
8
|
-
class Mongrel::HttpRequest
|
9
|
-
def put?
|
10
|
-
params["REQUEST_METHOD"] == "PUT"
|
11
|
-
end
|
12
|
-
|
13
|
-
def post?
|
14
|
-
params["REQUEST_METHOD"] == "POST"
|
15
|
-
end
|
16
|
-
|
17
|
-
def get?
|
18
|
-
params["REQUEST_METHOD"] == "GET"
|
19
|
-
end
|
20
|
-
|
21
|
-
def path_match pattern
|
22
|
-
params["REQUEST_PATH"].match(pattern)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
8
|
module Conveyor
|
27
|
-
|
28
|
-
|
29
|
-
|
9
|
+
class App
|
10
|
+
def initialize(data_directory, *options)
|
11
|
+
options = options.inject(){|(k, v), m| m[k] = v; m}
|
12
|
+
@data_directory = data_directory
|
13
|
+
@log_directory = options[:log_directory]
|
14
|
+
@unsafe_mode = options[:unsafe_mode] # allows deleting of channels. REALLY UNSAFE!
|
30
15
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if log_directory
|
37
|
-
@logger = Logger.new File.join(log_directory, 'conveyor.log')
|
38
|
-
else
|
39
|
-
@logger = Logger.new '/dev/null'
|
40
|
-
end
|
16
|
+
if @log_directory
|
17
|
+
@logger = Logger.new File.join(@log_directory, 'conveyor.log')
|
18
|
+
else
|
19
|
+
@logger = Logger.new '/dev/null'
|
20
|
+
end
|
41
21
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
22
|
+
@channels = {}
|
23
|
+
Dir.entries(@data_directory).each do |e|
|
24
|
+
if !['.', '..'].include?(e) && File.directory?(File.join(@data_directory, e)) && Channel.valid_channel_name?(e)
|
25
|
+
@channels[e] = Channel.new(File.join(@data_directory, e))
|
47
26
|
end
|
48
27
|
end
|
49
28
|
|
50
|
-
|
51
|
-
|
52
|
-
|
29
|
+
@requests = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def path_match env, pattern
|
33
|
+
env["REQUEST_PATH"].match(pattern)
|
34
|
+
end
|
53
35
|
|
54
|
-
|
55
|
-
|
36
|
+
def create_new_channel channel_name
|
37
|
+
@channels[channel_name] = Conveyor::Channel.new(File.join(@data_directory, channel_name))
|
38
|
+
end
|
39
|
+
|
40
|
+
def i msg
|
41
|
+
@logger.info msg
|
42
|
+
end
|
43
|
+
|
44
|
+
def put env, m
|
45
|
+
if Channel.valid_channel_name?(m.captures[0])
|
46
|
+
if !@channels.key?(m.captures[0])
|
47
|
+
create_new_channel m.captures[0]
|
48
|
+
i "#{env["REMOTE_ADDR"]} PUT #{env["REQUEST_PATH"]} 201"
|
49
|
+
[201, {}, "created channel #{m.captures[0]}"]
|
50
|
+
else
|
51
|
+
i "#{env["REMOTE_ADDR"]} PUT #{env["REQUEST_PATH"]}"
|
52
|
+
[202, {}, "channel already exists. didn't do anything"]
|
53
|
+
end
|
54
|
+
else
|
55
|
+
i "#{env["REMOTE_ADDR"]} GET #{env["REQUEST_PATH"]} 406"
|
56
|
+
[406, {}, "invalid channel name. must match #{Channel::NAME_PATTERN}"]
|
56
57
|
end
|
58
|
+
end
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
i "#{request.params["REMOTE_ADDR"]} PUT #{request.params["REQUEST_PATH"]} 201"
|
67
|
-
else
|
68
|
-
response.start(202) do |head, out|
|
69
|
-
out.write("channel already exists. didn't do anything")
|
70
|
-
end
|
71
|
-
i "#{request.params["REMOTE_ADDR"]} PUT #{request.params["REQUEST_PATH"]} "
|
72
|
-
end
|
60
|
+
def post env, m
|
61
|
+
if @channels.key?(m.captures[0])
|
62
|
+
params = Mongrel::HttpRequest.query_parse(env['QUERY_STRING'])
|
63
|
+
if params.key?('rewind_id')
|
64
|
+
if params['group']
|
65
|
+
@channels[m.captures[0]].rewind(:id => params['rewind_id'], :group => params['group']).to_i # TODO make sure this is an integer
|
66
|
+
[200, {}, "iterator rewound to #{params['rewind_id']}"]
|
73
67
|
else
|
74
|
-
|
75
|
-
|
76
|
-
i "#{request.params["REMOTE_ADDR"]} GET #{request.params["REQUEST_PATH"]} 406"
|
77
|
-
end
|
68
|
+
@channels[m.captures[0]].rewind(:id => params['rewind_id']).to_i # TODO make sure this is an integer
|
69
|
+
[200, {}, "iterator rewound to #{params['rewind_id']}"]
|
78
70
|
end
|
79
|
-
|
80
|
-
if
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
71
|
+
else
|
72
|
+
if env.key?('HTTP_DATE') && d = Time.parse(env['HTTP_DATE'])
|
73
|
+
id = @channels[m.captures[0]].post(env['rack.input'].read)
|
74
|
+
i "#{env["REMOTE_ADDR"]} POST #{env["REQUEST_PATH"]} 202"
|
75
|
+
[202, {"Location" => "/channels/#{m.captures[0]}/#{id}"}, ""]
|
76
|
+
else
|
77
|
+
i "#{env["REMOTE_ADDR"]} POST #{env["REQUEST_PATH"]} 400"
|
78
|
+
[400, {}, "A valid Date header is required for all POSTs."]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
elsif Channel.valid_channel_name?(m.captures[0])
|
82
|
+
create_new_channel(m.captures[0])
|
83
|
+
post(env, m)
|
84
|
+
else
|
85
|
+
[404, {}, '']
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def get env
|
90
|
+
headers = content = nil
|
91
|
+
if m = path_match(env, %r{/channels/(.*)/(\d+)})
|
92
|
+
if @channels.key?(m.captures[0])
|
93
|
+
headers, content = @channels[m.captures[0]].get(m.captures[1].to_i)
|
94
|
+
end
|
95
|
+
elsif m = path_match(env, %r{/channels/(.*)})
|
96
|
+
if @channels.key?(m.captures[0])
|
97
|
+
params = Mongrel::HttpRequest.query_parse(env['QUERY_STRING'])
|
98
|
+
if params.key? 'next'
|
99
|
+
if params.key? 'group'
|
100
|
+
if params.key? 'n'
|
101
|
+
list = @channels[m.captures[0]].get_next_n_by_group(params['n'].to_i, params['group'])
|
88
102
|
else
|
89
|
-
@channels[m.captures[0]].
|
90
|
-
response.start(200) do |head, out|
|
91
|
-
out.write "iterator rewound to #{params['rewind_id']}"
|
92
|
-
end
|
103
|
+
headers, content = @channels[m.captures[0]].get_next_by_group(params['group'])
|
93
104
|
end
|
94
105
|
else
|
95
|
-
if
|
96
|
-
|
97
|
-
|
98
|
-
head["Location"] = "/channels/#{m.captures[0]}/#{id}"
|
106
|
+
if params.key? 'n'
|
107
|
+
list = @channels[m.captures[0]].get_next_n(params['n'].to_i).map do |i|
|
108
|
+
{:data => i[1], :hash => i[0][:hash], :id => i[0][:id]}
|
99
109
|
end
|
100
|
-
i "#{request.params["REMOTE_ADDR"]} POST #{request.params["REQUEST_PATH"]} 202"
|
101
110
|
else
|
102
|
-
|
103
|
-
out.write "A valid Date header is required for all POSTs."
|
104
|
-
end
|
105
|
-
i "#{request.params["REMOTE_ADDR"]} POST #{request.params["REQUEST_PATH"]} 400"
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
elsif request.get?
|
111
|
-
headers = content = nil
|
112
|
-
if m = request.path_match(%r{/channels/(.*)/(\d+)})
|
113
|
-
if @channels.key?(m.captures[0])
|
114
|
-
headers, content = @channels[m.captures[0]].get(m.captures[1].to_i)
|
115
|
-
end
|
116
|
-
elsif m = request.path_match(%r{/channels/(.*)})
|
117
|
-
if @channels.key?(m.captures[0])
|
118
|
-
params = Mongrel::HttpRequest.query_parse(request.params['QUERY_STRING'])
|
119
|
-
if params.key? 'next'
|
120
|
-
if params.key? 'group'
|
121
|
-
if params.key? 'n'
|
122
|
-
list = @channels[m.captures[0]].get_next_n_by_group(params['n'].to_i, params['group'])
|
123
|
-
else
|
124
|
-
headers, content = @channels[m.captures[0]].get_next_by_group(params['group'])
|
125
|
-
end
|
126
|
-
else
|
127
|
-
if params.key? 'n'
|
128
|
-
list = @channels[m.captures[0]].get_next_n(params['n'].to_i)
|
129
|
-
list = list.map do |i|
|
130
|
-
{:data => i[1], :hash => i[0][:hash], :id => i[0][:id]}
|
131
|
-
end
|
132
|
-
else
|
133
|
-
headers, content = @channels[m.captures[0]].get_next
|
134
|
-
end
|
135
|
-
end
|
136
|
-
else
|
137
|
-
response.start(200) do |head, out|
|
138
|
-
out.write @channels[m.captures[0]].status.to_json
|
139
|
-
end
|
111
|
+
headers, content = @channels[m.captures[0]].get_next
|
140
112
|
end
|
141
113
|
end
|
142
114
|
else
|
143
|
-
|
144
|
-
out.write("fake!")
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
if headers && content
|
149
|
-
response.start(200) do |head, out|
|
150
|
-
head['Content-Location'] = "/channels/#{m.captures[0]}/#{headers[:id]}"
|
151
|
-
head['Content-MD5'] = headers[:hash]
|
152
|
-
head['Content-Type'] = 'application/octet-stream'
|
153
|
-
head['Last-Modified'] = Time.parse(headers[:time]).gmtime.to_s
|
154
|
-
out.write content
|
155
|
-
end
|
156
|
-
i "#{request.params["REMOTE_ADDR"]} GET #{request.params["REQUEST_PATH"]} 200 #{headers[:id]} #{headers[:length]} #{headers[:hash]}"
|
157
|
-
elsif list
|
158
|
-
response.start(200) do |head, out|
|
159
|
-
out.write list.to_json
|
160
|
-
end
|
115
|
+
return [200, {}, @channels[m.captures[0]].status.to_json]
|
161
116
|
end
|
162
|
-
|
163
117
|
end
|
118
|
+
else
|
119
|
+
return [200, {}, "fake!"]
|
164
120
|
end
|
121
|
+
|
122
|
+
if headers && content
|
123
|
+
i "#{env["REMOTE_ADDR"]} GET #{env["REQUEST_PATH"]} 200 #{headers[:id]} #{headers[:length]} #{headers[:hash]}"
|
124
|
+
return [
|
125
|
+
200,
|
126
|
+
{
|
127
|
+
'Content-Location' => "/channels/#{m.captures[0]}/#{headers[:id]}",
|
128
|
+
'Content-MD5' => headers[:hash],
|
129
|
+
'Content-Type' => 'application/octet-stream',
|
130
|
+
'Last-Modified' => Time.at(headers[:time]).gmtime.to_s,
|
131
|
+
},
|
132
|
+
content
|
133
|
+
]
|
134
|
+
elsif list
|
135
|
+
return [200, {}, list.to_json]
|
136
|
+
end
|
137
|
+
|
138
|
+
return [404, {}, '']
|
165
139
|
end
|
166
140
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
141
|
+
def delete env, m
|
142
|
+
if @channels.key?(m.captures[0])
|
143
|
+
@channels[m.captures[0]].delete!
|
144
|
+
@channels.delete(m.captures[0])
|
145
|
+
[200, {}, "Channel deleted."]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def call(env)
|
150
|
+
@requests += 1
|
151
|
+
if env['REQUEST_METHOD'] == 'PUT' && m = path_match(env, %r{/channels/(.*)})
|
152
|
+
put(env, m)
|
153
|
+
elsif env['REQUEST_METHOD'] == 'POST' && m = path_match(env, %r{/channels/(.*)})
|
154
|
+
post(env, m)
|
155
|
+
elsif @unsafe_mode && env['REQUEST_METHOD'] == 'DELETE' && m = path_match(env, %r{/channels/(.*)})
|
156
|
+
delete(env, m)
|
157
|
+
elsif env['REQUEST_METHOD'] == 'GET'
|
158
|
+
get(env)
|
159
|
+
else
|
160
|
+
[404, {}, '']
|
161
|
+
end
|
173
162
|
end
|
174
163
|
end
|
175
164
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'conveyor/base_channel'
|
2
|
+
|
3
|
+
def String.random_alphanumeric(size=16)
|
4
|
+
s = ""
|
5
|
+
size.times { s << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }
|
6
|
+
s
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
module Conveyor
|
11
|
+
class Upgrader
|
12
|
+
|
13
|
+
def initialize directory
|
14
|
+
@directory = directory
|
15
|
+
end
|
16
|
+
|
17
|
+
def version_path
|
18
|
+
File.join(@directory, 'version')
|
19
|
+
end
|
20
|
+
|
21
|
+
def source_version
|
22
|
+
if File.exists?(version_path) && File.size(version_path) > 0
|
23
|
+
File.open(version_path).read.to_i
|
24
|
+
else
|
25
|
+
0
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def upgrade
|
30
|
+
case source_version
|
31
|
+
when 0
|
32
|
+
from_0
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_headers_0 str, index_file = false
|
37
|
+
pattern = '\A(\d+) (\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}[+\-]\d{2}\:\d{2}) (\d+) (\d+) ([a-f0-9]+)'
|
38
|
+
pattern += ' (\d+)' if index_file
|
39
|
+
pattern += '\Z'
|
40
|
+
m = str.match(Regexp.new(pattern))
|
41
|
+
{
|
42
|
+
:id => m.captures[0].to_i,
|
43
|
+
:time => m.captures[1],
|
44
|
+
:offset => m.captures[2].to_i,
|
45
|
+
:length => m.captures[3].to_i,
|
46
|
+
:hash => m.captures[4],
|
47
|
+
:file => (index_file ? m.captures[5].to_i : nil)
|
48
|
+
}.reject {|k,v| v == nil}
|
49
|
+
end
|
50
|
+
|
51
|
+
def from_0
|
52
|
+
Dir.glob(@directory + "/*").each do |d|
|
53
|
+
|
54
|
+
if File.directory?(d)
|
55
|
+
# create tmp dir
|
56
|
+
tmp_dir = create_tmp_dir!
|
57
|
+
puts "writing to #{tmp_dir}"
|
58
|
+
chan = Channel.new(tmp_dir)
|
59
|
+
|
60
|
+
Dir.glob("#{d}/[0-9]*").each do |f|
|
61
|
+
puts "upgrading #{f}"
|
62
|
+
size = File.size(f)
|
63
|
+
f = File.open(f)
|
64
|
+
|
65
|
+
while f.pos < size
|
66
|
+
l = f.readline.strip
|
67
|
+
header = parse_headers_0(l)
|
68
|
+
content = f.read(header[:length])
|
69
|
+
f.readline # newline chomp
|
70
|
+
|
71
|
+
chan.commit(content, Time.parse(header[:time]))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
puts "upgrading iterator"
|
76
|
+
iterator = File.open(File.join(d, 'iterator')) do |f|
|
77
|
+
l = nil
|
78
|
+
while !f.eof? && l = f.readline
|
79
|
+
end
|
80
|
+
if l
|
81
|
+
chan.rewind :id => l.strip.to_i
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Dir.glob(File.join(d, 'iterator-*')) do |i|
|
86
|
+
group = i.split('/').last.split('-').last
|
87
|
+
puts "upgrading group iterator for #{group}"
|
88
|
+
iterator = File.open(i) do |f|
|
89
|
+
l = nil
|
90
|
+
while !f.eof? && l = f.readline
|
91
|
+
end
|
92
|
+
if l
|
93
|
+
chan.rewind :id => l.strip.to_i, :group => group
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
puts "backing up #{d} to #{d}.bak"
|
99
|
+
FileUtils.mv d, d + ".bak"
|
100
|
+
|
101
|
+
puts "copying from #{tmp_dir} to #{d}"
|
102
|
+
FileUtils.cp_r tmp_dir, d
|
103
|
+
|
104
|
+
puts "deleting temp data"
|
105
|
+
FileUtils.rm_r tmp_dir
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_tmp_dir!
|
111
|
+
loop do
|
112
|
+
tmp_dir = File.join('/tmp', String.random_alphanumeric)
|
113
|
+
if !File.exists?(tmp_dir)
|
114
|
+
return tmp_dir
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|