madeleine 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -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
+