conveyor 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
@@ -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
@@ -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
@@ -9,7 +9,7 @@ Hoe.new('conveyor', Conveyor::VERSION) do |p|
9
9
  p.author = 'Ryan King'
10
10
  p.email = 'ryan@theryanking.com'
11
11
  p.remote_rdoc_dir = ''
12
- p.extra_deps << ['mongrel']
12
+ p.extra_deps << ['thin']
13
13
  p.extra_deps << ['json']
14
14
  p.extra_deps << ['daemons']
15
15
  end
@@ -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
- server = Conveyor::Server.new('0.0.0.0', options[:port], ARGV[0], options[:log_directory])
43
-
44
- Signal.trap 'INT' do
45
- server.stop
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
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby -KU
2
+
3
+ $: << 'lib'
4
+
5
+ require 'conveyor/server'
6
+ require 'conveyor/upgrader'
7
+
8
+ u = Conveyor::Upgrader.new ARGV.first
9
+
10
+ u.upgrade
@@ -1,4 +1,4 @@
1
1
  module Conveyor
2
- VERSION = '0.1.4'
2
+ VERSION = '0.2.0'
3
3
  QUALITY = 'alpha'
4
4
  end
@@ -1,7 +1,9 @@
1
1
  require 'digest/md5'
2
2
  require 'rubygems'
3
3
  require 'time'
4
- require 'priority_queue'
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 = directory
17
- @data_files = []
18
- @index = []
19
- @iterator = 1
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
- index_path = File.join(@directory, 'index')
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
- Thread.exclusive do
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.xmlschema} #{o} #{l} #{h}"
75
- f.write("#{header}\n" + data + "\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.find{|e| e[:id] == id}
88
- header, content = nil
89
- Thread.exclusive do
90
- bucket_file(i[:file]) do |f|
91
- f.seek i[:offset]
92
- header = f.readline.strip
93
- content = f.read(i[:length])
94
- end
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{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}[+\-]\d{2}\:\d{2}) (\d+) (\d+) ([a-f0-9]+)'
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 => m.captures[0].to_i,
106
- :time => m.captures[1],
107
- :offset => m.captures[2].to_i,
108
- :length => m.captures[3].to_i,
109
- :hash => m.captures[4],
110
- :file => (index_file ? m.captures[5].to_i : nil)
111
- }.reject {|k,v| v == nil}
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
@@ -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
- super(directory)
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
- if File.exists?(iterator_path) && File.size(iterator_path) > 0
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
- Dir.glob(File.join(@directory, 'iterator-*')) do |i|
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
- Thread.exclusive do
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
- Thread.exclusive do
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
- Thread.exclusive do
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
- Thread.exclusive do
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
- Thread.exclusive do
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
- Thread.exclusive do
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