madeleine 0.7.1
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/COPYING +31 -0
- data/NEWS +55 -0
- data/README +78 -0
- data/contrib/batched.rb +298 -0
- data/contrib/benchmark.rb +35 -0
- data/contrib/test_batched.rb +245 -0
- data/contrib/test_scalability.rb +248 -0
- data/contrib/threaded_benchmark.rb +44 -0
- data/lib/madeleine.rb +419 -0
- data/lib/madeleine/automatic.rb +418 -0
- data/lib/madeleine/clock.rb +94 -0
- data/lib/madeleine/files.rb +19 -0
- data/lib/madeleine/zmarshal.rb +60 -0
- data/samples/clock_click.rb +73 -0
- data/samples/dictionary_client.rb +23 -0
- data/samples/dictionary_server.rb +94 -0
- data/samples/painter.rb +60 -0
- metadata +53 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift("../lib")
|
4
|
+
|
5
|
+
require 'madeleine'
|
6
|
+
require 'batched'
|
7
|
+
|
8
|
+
class BenchmarkCommand
|
9
|
+
def initialize(value)
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(system)
|
14
|
+
# do nothing
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
madeleine = BattchedSnapshotMadeleine.new("benchmark-base") { :the_system }
|
19
|
+
|
20
|
+
RUNS = 200
|
21
|
+
THREADS = 10
|
22
|
+
|
23
|
+
GC.start
|
24
|
+
GC.disable
|
25
|
+
|
26
|
+
t0 = Time.now
|
27
|
+
|
28
|
+
threads = []
|
29
|
+
THREADS.times {
|
30
|
+
threads << Thread.new {
|
31
|
+
RUNS.times {
|
32
|
+
madeleine.execute_command(BenchmarkCommand.new(1234))
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
threads.each {|t| t.join }
|
37
|
+
t1 = Time.now
|
38
|
+
|
39
|
+
GC.enable
|
40
|
+
|
41
|
+
tps = (THREADS * RUNS)/(t1 - t0)
|
42
|
+
|
43
|
+
puts "#{tps.to_i} transactions/s"
|
44
|
+
|
data/lib/madeleine.rb
ADDED
@@ -0,0 +1,419 @@
|
|
1
|
+
#
|
2
|
+
# Madeleine - Ruby Object Prevalence
|
3
|
+
#
|
4
|
+
# Author:: Anders Bengtsson <ndrsbngtssn@yahoo.se>
|
5
|
+
# Copyright:: Copyright (c) 2003-2004
|
6
|
+
#
|
7
|
+
# Usage:
|
8
|
+
#
|
9
|
+
# require 'madeleine'
|
10
|
+
#
|
11
|
+
# madeleine = SnapshotMadeleine.new("my_example_storage") {
|
12
|
+
# SomeExampleApplication.new()
|
13
|
+
# }
|
14
|
+
#
|
15
|
+
# madeleine.execute_command(command)
|
16
|
+
#
|
17
|
+
|
18
|
+
module Madeleine
|
19
|
+
|
20
|
+
require 'thread'
|
21
|
+
require 'sync'
|
22
|
+
require 'madeleine/files'
|
23
|
+
|
24
|
+
MADELEINE_VERSION = "0.7.1"
|
25
|
+
|
26
|
+
class SnapshotMadeleine
|
27
|
+
|
28
|
+
# Builds a new Madeleine instance. If there is a snapshot available
|
29
|
+
# then the system will be created from that, otherwise
|
30
|
+
# <tt>new_system</tt> will be used. The state of the system will
|
31
|
+
# then be restored from the command logs.
|
32
|
+
#
|
33
|
+
# You can provide your own snapshot marshaller, for instance using
|
34
|
+
# YAML or SOAP, instead of Ruby's built-in marshaller. The
|
35
|
+
# <tt>snapshot_marshaller</tt> must respond to
|
36
|
+
# <tt>load(stream)</tt> and <tt>dump(object, stream)</tt>. You
|
37
|
+
# must use the same marshaller every time for a system.
|
38
|
+
#
|
39
|
+
# See: DefaultSnapshotMadeleine
|
40
|
+
#
|
41
|
+
# * <tt>directory_name</tt> - Storage directory to use. Will be created if needed.
|
42
|
+
# * <tt>snapshot_marshaller</tt> - Marshaller to use for system snapshots. (Optional)
|
43
|
+
# * <tt>new_system_block</tt> - Block to create a new system (if no stored system was found).
|
44
|
+
def self.new(directory_name, snapshot_marshaller=Marshal, &new_system_block)
|
45
|
+
log_factory = DefaultLogFactory.new
|
46
|
+
logger = Logger.new(directory_name,
|
47
|
+
log_factory)
|
48
|
+
snapshotter = Snapshotter.new(directory_name,
|
49
|
+
snapshot_marshaller)
|
50
|
+
lock = DefaultLock.new
|
51
|
+
recoverer = Recoverer.new(directory_name,
|
52
|
+
snapshot_marshaller)
|
53
|
+
system = recoverer.recover_snapshot(new_system_block)
|
54
|
+
executer = Executer.new(system)
|
55
|
+
recoverer.recover_logs(executer)
|
56
|
+
DefaultSnapshotMadeleine.new(system, logger, snapshotter, lock, executer)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class DefaultSnapshotMadeleine
|
61
|
+
|
62
|
+
# The prevalent system
|
63
|
+
attr_reader :system
|
64
|
+
|
65
|
+
def initialize(system, logger, snapshotter, lock, executer)
|
66
|
+
@system = system
|
67
|
+
@logger = logger
|
68
|
+
@snapshotter = snapshotter
|
69
|
+
@lock = lock
|
70
|
+
@executer = executer
|
71
|
+
|
72
|
+
@closed = false
|
73
|
+
end
|
74
|
+
|
75
|
+
# Execute a command on the prevalent system.
|
76
|
+
#
|
77
|
+
# Commands must have a method <tt>execute(aSystem)</tt>.
|
78
|
+
# Otherwise an error, <tt>Madeleine::InvalidCommandException</tt>,
|
79
|
+
# will be raised.
|
80
|
+
#
|
81
|
+
# The return value from the command's <tt>execute()</tt> method is returned.
|
82
|
+
#
|
83
|
+
# * <tt>command</tt> - The command to execute on the system.
|
84
|
+
def execute_command(command)
|
85
|
+
verify_command_sane(command)
|
86
|
+
@lock.synchronize {
|
87
|
+
raise "closed" if @closed
|
88
|
+
@logger.store(command)
|
89
|
+
@executer.execute(command)
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Execute a query on the prevalent system.
|
94
|
+
#
|
95
|
+
# Only differs from <tt>execute_command</tt> in that the command/query isn't logged, and
|
96
|
+
# therefore isn't allowed to modify the system. A shared lock is held, preventing others
|
97
|
+
# from modifying the system while the query is running.
|
98
|
+
#
|
99
|
+
# * <tt>query</tt> - The query command to execute
|
100
|
+
def execute_query(query)
|
101
|
+
@lock.synchronize_shared {
|
102
|
+
@executer.execute(query)
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
# Take a snapshot of the current system.
|
107
|
+
#
|
108
|
+
# You need to regularly take a snapshot of a running system,
|
109
|
+
# otherwise the logs will grow big and restarting the system will take a
|
110
|
+
# long time. Your backups must also be done from the snapshot files,
|
111
|
+
# since you can't make a consistent backup of a live log.
|
112
|
+
#
|
113
|
+
# A practical way of doing snapshots is a timer thread:
|
114
|
+
#
|
115
|
+
# Thread.new(madeleine) {|madeleine|
|
116
|
+
# while true
|
117
|
+
# sleep(60 * 60 * 24) # 24 hours
|
118
|
+
# madeleine.take_snapshot
|
119
|
+
# end
|
120
|
+
# }
|
121
|
+
def take_snapshot
|
122
|
+
@lock.synchronize {
|
123
|
+
@logger.close
|
124
|
+
@snapshotter.take(@system)
|
125
|
+
@logger.reset
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
# Close the system.
|
130
|
+
#
|
131
|
+
# The log file is closed and no new commands can be received
|
132
|
+
# by this Madeleine.
|
133
|
+
def close
|
134
|
+
@lock.synchronize {
|
135
|
+
@logger.close
|
136
|
+
@closed = true
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def verify_command_sane(command)
|
143
|
+
unless command.respond_to?(:execute)
|
144
|
+
raise InvalidCommandException.new("Commands must have an 'execute' method")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class InvalidCommandException < Exception
|
150
|
+
end
|
151
|
+
|
152
|
+
#
|
153
|
+
# Internal classes below
|
154
|
+
#
|
155
|
+
|
156
|
+
FILE_COUNTER_SIZE = 21 #:nodoc:
|
157
|
+
|
158
|
+
class DefaultLock #:nodoc:
|
159
|
+
|
160
|
+
def initialize
|
161
|
+
@lock = Sync.new
|
162
|
+
end
|
163
|
+
|
164
|
+
def synchronize(&block)
|
165
|
+
@lock.synchronize(&block)
|
166
|
+
end
|
167
|
+
|
168
|
+
def synchronize_shared(&block)
|
169
|
+
@lock.synchronize(:SH, &block)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class Executer #:nodoc:
|
174
|
+
|
175
|
+
def initialize(system)
|
176
|
+
@system = system
|
177
|
+
@in_recovery = false
|
178
|
+
end
|
179
|
+
|
180
|
+
def execute(command)
|
181
|
+
begin
|
182
|
+
command.execute(@system)
|
183
|
+
rescue
|
184
|
+
raise unless @in_recovery
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def recovery
|
189
|
+
begin
|
190
|
+
@in_recovery = true
|
191
|
+
yield
|
192
|
+
ensure
|
193
|
+
@in_recovery = false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class Recoverer #:nodoc:
|
199
|
+
|
200
|
+
def initialize(directory_name, marshaller)
|
201
|
+
@directory_name, @marshaller = directory_name, marshaller
|
202
|
+
end
|
203
|
+
|
204
|
+
def recover_snapshot(new_system_block)
|
205
|
+
system = nil
|
206
|
+
id = SnapshotFile.highest_id(@directory_name)
|
207
|
+
if id > 0
|
208
|
+
snapshot_file = SnapshotFile.new(@directory_name, id).name
|
209
|
+
open(snapshot_file, "rb") {|snapshot|
|
210
|
+
system = @marshaller.load(snapshot)
|
211
|
+
}
|
212
|
+
else
|
213
|
+
system = new_system_block.call
|
214
|
+
end
|
215
|
+
system
|
216
|
+
end
|
217
|
+
|
218
|
+
def recover_logs(executer)
|
219
|
+
executer.recovery {
|
220
|
+
CommandLog.log_file_names(@directory_name, FileService.new).each {|file_name|
|
221
|
+
open(@directory_name + File::SEPARATOR + file_name, "rb") {|log|
|
222
|
+
recover_log(executer, log)
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
def recover_log(executer, log)
|
231
|
+
while ! log.eof?
|
232
|
+
command = Marshal.load(log)
|
233
|
+
executer.execute(command)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
class NumberedFile #:nodoc:
|
239
|
+
|
240
|
+
def initialize(path, name, id)
|
241
|
+
@path, @name, @id = path, name, id
|
242
|
+
end
|
243
|
+
|
244
|
+
def name
|
245
|
+
result = @path
|
246
|
+
result += File::SEPARATOR
|
247
|
+
result += sprintf("%0#{FILE_COUNTER_SIZE}d", @id)
|
248
|
+
result += '.'
|
249
|
+
result += @name
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class CommandLog #:nodoc:
|
254
|
+
|
255
|
+
def self.log_file_names(directory_name, file_service)
|
256
|
+
return [] unless file_service.exist?(directory_name)
|
257
|
+
result = file_service.dir_entries(directory_name).select {|name|
|
258
|
+
name =~ /^\d{#{FILE_COUNTER_SIZE}}\.command_log$/
|
259
|
+
}
|
260
|
+
result.each {|name| name.untaint }
|
261
|
+
result.sort!
|
262
|
+
result
|
263
|
+
end
|
264
|
+
|
265
|
+
def initialize(path, file_service)
|
266
|
+
id = self.class.highest_log(path, file_service) + 1
|
267
|
+
numbered_file = NumberedFile.new(path, "command_log", id)
|
268
|
+
@file = file_service.open(numbered_file.name, 'wb')
|
269
|
+
end
|
270
|
+
|
271
|
+
def close
|
272
|
+
@file.close
|
273
|
+
end
|
274
|
+
|
275
|
+
def store(command)
|
276
|
+
Marshal.dump(command, @file)
|
277
|
+
@file.flush
|
278
|
+
@file.fsync
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.highest_log(directory_name, file_service)
|
282
|
+
highest = 0
|
283
|
+
log_file_names(directory_name, file_service).each {|file_name|
|
284
|
+
match = /^(\d{#{FILE_COUNTER_SIZE}})/.match(file_name)
|
285
|
+
n = match[1].to_i
|
286
|
+
if n > highest
|
287
|
+
highest = n
|
288
|
+
end
|
289
|
+
}
|
290
|
+
highest
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class DefaultLogFactory #:nodoc:
|
295
|
+
def create_log(directory_name)
|
296
|
+
CommandLog.new(directory_name, FileService.new)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
class Logger #:nodoc:
|
301
|
+
|
302
|
+
def initialize(directory_name, log_factory)
|
303
|
+
@directory_name = directory_name
|
304
|
+
@log_factory = log_factory
|
305
|
+
@log = nil
|
306
|
+
@pending_tick = nil
|
307
|
+
ensure_directory_exists
|
308
|
+
end
|
309
|
+
|
310
|
+
def ensure_directory_exists
|
311
|
+
if ! File.exist?(@directory_name)
|
312
|
+
Dir.mkdir(@directory_name)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def reset
|
317
|
+
close
|
318
|
+
delete_log_files
|
319
|
+
end
|
320
|
+
|
321
|
+
def store(command)
|
322
|
+
if command.kind_of?(Madeleine::Clock::Tick)
|
323
|
+
@pending_tick = command
|
324
|
+
else
|
325
|
+
if @pending_tick
|
326
|
+
internal_store(@pending_tick)
|
327
|
+
@pending_tick = nil
|
328
|
+
end
|
329
|
+
internal_store(command)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def internal_store(command)
|
334
|
+
if @log.nil?
|
335
|
+
open_new_log
|
336
|
+
end
|
337
|
+
@log.store(command)
|
338
|
+
end
|
339
|
+
|
340
|
+
def close
|
341
|
+
return if @log.nil?
|
342
|
+
@log.close
|
343
|
+
@log = nil
|
344
|
+
end
|
345
|
+
|
346
|
+
private
|
347
|
+
|
348
|
+
def delete_log_files
|
349
|
+
Dir.glob(@directory_name + File::SEPARATOR + "*.command_log").each {|name|
|
350
|
+
name.untaint
|
351
|
+
File.delete(name)
|
352
|
+
}
|
353
|
+
end
|
354
|
+
|
355
|
+
def open_new_log
|
356
|
+
@log = @log_factory.create_log(@directory_name)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
class SnapshotFile < NumberedFile #:nodoc:
|
361
|
+
|
362
|
+
def self.highest_id(directory_name)
|
363
|
+
return 0 unless File.exist?(directory_name)
|
364
|
+
suffix = "snapshot"
|
365
|
+
highest = 0
|
366
|
+
Dir.foreach(directory_name) {|file_name|
|
367
|
+
match = /^(\d{#{FILE_COUNTER_SIZE}}\.#{suffix}$)/.match(file_name)
|
368
|
+
next unless match
|
369
|
+
n = match[1].to_i
|
370
|
+
if n > highest
|
371
|
+
highest = n
|
372
|
+
end
|
373
|
+
}
|
374
|
+
highest
|
375
|
+
end
|
376
|
+
|
377
|
+
def self.next(directory_name)
|
378
|
+
new(directory_name, highest_id(directory_name) + 1)
|
379
|
+
end
|
380
|
+
|
381
|
+
def initialize(directory_name, id)
|
382
|
+
super(directory_name, "snapshot", id)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
class Snapshotter #:nodoc:
|
387
|
+
|
388
|
+
def initialize(directory_name, marshaller)
|
389
|
+
@directory_name, @marshaller = directory_name, marshaller
|
390
|
+
end
|
391
|
+
|
392
|
+
def take(system)
|
393
|
+
numbered_file = SnapshotFile.next(@directory_name)
|
394
|
+
name = numbered_file.name
|
395
|
+
open(name + '.tmp', 'wb') {|snapshot|
|
396
|
+
@marshaller.dump(system, snapshot)
|
397
|
+
snapshot.flush
|
398
|
+
snapshot.fsync
|
399
|
+
}
|
400
|
+
File.rename(name + '.tmp', name)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
module Clock #:nodoc:
|
405
|
+
class Tick #:nodoc:
|
406
|
+
|
407
|
+
def initialize(time)
|
408
|
+
@time = time
|
409
|
+
end
|
410
|
+
|
411
|
+
def execute(system)
|
412
|
+
system.clock.forward_to(@time)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
SnapshotMadeleine = Madeleine::SnapshotMadeleine
|
419
|
+
|