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.
@@ -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
+