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 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