Deleting Madeleine... with pleasure (it is cool, but not for wiki data)

This commit is contained in:
Rick Okin 2005-08-10 05:28:05 +00:00
parent b94559bc4c
commit 2c7a2779c7
35 changed files with 2 additions and 4988 deletions

View file

@ -7,7 +7,7 @@ spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'instiki'
s.version = "0.10.2"
s.summary = 'Easy to install WikiClone running on WEBrick and Madeleine'
s.summary = 'Easy to install WikiClone running on WEBrick and SQLite'
s.description = <<-EOF
Instiki is a Wiki Clone written in Ruby that ships with an embedded
webserver. You can setup up an Instiki in just a few steps.
@ -24,10 +24,10 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = false
s.add_dependency('madeleine', '= 0.7.1')
s.add_dependency('RedCloth', '= 3.0.3')
s.add_dependency('rubyzip', '= 0.5.8')
s.add_dependency('rails', '= 0.13.1')
s.add_dependency('sqlite3-ruby', '= 1.1.0')
s.requirements << 'none'
s.require_path = 'lib'

View file

@ -1,2 +0,0 @@
PrevalenceBase
*.gem

View file

@ -1,31 +0,0 @@
Copyright (c) 2003-2004, Anders Bengtsson
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The names of its contributors may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.

View file

@ -1,55 +0,0 @@
Madeleine 0.7.1 (August 22, 2004):
* ZMarshal changed to work around Zlib bug.
* automatic_read_only fixed when intercepted class is inherited from
Madeleine 0.7 (July 23, 2004):
* Broken clock unit test on win32 fixed.
* AutomaticSnapshotMadeleine detects snapshot format on recovery
* Snapshot compression with Madeleine::ZMarshal
* YAML snapshots supported for automatic commands
* SOAP snapshots supported for automatic commands
* Read-only methods for automatic commands
Madeleine 0.6.1 (March 30, 2004):
* Bug fix: Use binary mode for I/O, fixes log replay
on mswin32 port of Ruby (Patch from Martin Tampe)
Madeleine 0.6 (March 28, 2004):
* Changed license to BSD
* Added a RubyGem specification
* Re-designed initialization (but still backward-compatible)
* Bug fix: Fixed use of finalized object's id in AutomaticSnapshotMadeleine
Madeleine 0.5 (August 31, 2003):
* Bug fix: Log order on recovery was wrong on some platforms
(Reported by IIMA Susumu)
* No longer requires the system clock to always increase
* Shared locks for queries
Madeleine 0.4 (July 4, 2003):
* Deprecated ClockedSnapshotMadeleine
* Added execute_query()
* API documentation in RDoc format
Madeleine 0.3 (May 15, 2003):
* Automatic commands
* Some classes exported to the default module
* Clock support not loaded by default (require 'madeleine/clock')
* Bug fix: Error handling when replaying logged commands.
* New system through block instead of argument (API change)
* Works in $SAFE = 1
Madeleine 0.2:
* Supports custom marshalling implementations.
* Changed interface for ClockedSystem and Clock.
* Some documentation added, including API docs.

View file

@ -1,78 +0,0 @@
Madeleine is a Ruby implementation of Object Prevalence: Transparent
persistence of business objects using command logging and complete
system snapshots.
<http://madeleine.sourceforge.net/>
Madeleine's design is based on Prevayler, the original Java
prevalence layer.
Learn more about object prevalence at <http://www.prevayler.org/>.
Installation:
Typical installation procedure is:
$ ruby install.rb config
$ ruby install.rb setup
# ruby install.rb install (may require root privilege)
Try 'ruby install.rb --help' for detailed usage.
[From the documentation of Minero Aoki's 'install.rb']
Usage:
require 'madeleine'
# Create an application as a prevalent system
madeleine = SnapshotMadeleine.new("my_example_storage") {
SomeExampleApplication.new()
}
# Do modifications of the system by sending commands through
# the Madeleine instance. A command is an object with a suitable
# "execute(system)" method.
madeleine.execute_command(command)
Requirements:
* Ruby 1.8.1 or later
Additionaly, some of the sample code also uses ruby/tk.
Known problems:
* Won't run in some Windows-ports of Ruby due to missing
fsync() call.
Contact:
Homepage:
<http://madeleine.sourceforge.net/>
Questions, bug reports, patches, complaints? Use the mailing list:
<http://lists.sourceforge.net/lists/listinfo/madeleine-devel>
License:
BSD (see the file COPYING)
Credits:
Anders Bengtsson - Prevalence core impl.
Stephen Sykes - Automatic commands impl.
With the help of patches, testing and feedback from:
Steve Conover, David Heinemeier Hansson, Johan Lind, Håkan Råberg,
IIMA Susumu, Martin Tampe and Jon Tirsén
Thanks to Klaus Wuestefeld and the Prevayler developers for the
model of this software; to Minero Aoki for the installer; to Matz and
the core developers for the Ruby language!

View file

@ -1,23 +0,0 @@
- Fix broken time-dependent unit test
* Rolling snapshots, with age limit
- Compressed snapshots
- Full support for YAML snapshots
- SOAP marshalling
* Configurable log marshaller (or use the snapshot marshaller?)
* Write a document about the different marshallers, for app. developers.
* Move all default implementations into a "Default" module
* Introduce an object representing a log directory
* Move recovery out of DefaultSnapshotMadeleine entirely
* Write an example with a web server
* Replace filesystem with mock objects for unit testing.
* ClockCommand
* Integrate batched-writes in SnapshotMadeleine
* More sample code
* More documentation
* DRb integration
* Rollback
* Handle broken logs?

View file

@ -1,298 +0,0 @@
# Batched writes for Madeleine
#
# Copyright(c) Håkan Råberg 2003
#
#
# This is an experimental implementation of batched log writes to mininize
# calls to fsync. It uses a Shared/Exclusive-Lock, implemented in sync.rb,
# which is included in Ruby 1.8.
#
# Writes are batched for a specified amount of time, before written to disk and
# then executed.
#
# For a detailed discussion about the problem, see
# http://www.prevayler.org/wiki.jsp?topic=OvercomingTheWriteBottleneck
#
#
# Usage is identical to normal SnapshotMadeleine, and it can also be used as
# persister for AutomaticSnapshotMadeleine. (One difference: the log isn't
# visible on disk until any commands are executed.)
#
# You can also use the execute_query method for shared synchronzied queries,
# for eaay coarse-grained locking of the system.
#
# The exclusive lock is only locked during the actual execution of commands and
# while closing.
#
# Keeping both log writes and executes of commands in the originating thread
# is needed by AutomaticSnapshotPrevayler. Hence the strange SimplisticPipe
# class.
#
# Todo:
# - It seems like Sync (sync.rb) prefers shared locks. This should probably
# be changed.
#
#
# Madeleine - Ruby Object Prevalence
#
# Copyright(c) Anders Bengtsson 2003
#
require 'madeleine'
require 'madeleine/clock'
include Madeleine::Clock
module Madeleine
module Batch
class BatchedSnapshotMadeleine < SnapshotMadeleine
def initialize(directory_name, marshaller=Marshal, &new_system_block)
super(directory_name, marshaller, &new_system_block)
@log_actor = LogActor.launch(self)
end
def execute_command(command)
verify_command_sane(command)
queued_command = QueuedCommand.new(command)
@lock.synchronize(:SH) do
raise "closed" if @closed
@logger.store(queued_command)
end
queued_command.wait_for
end
def execute_query(query)
verify_command_sane(query)
@lock.synchronize(:SH) do
execute_without_storing(query)
end
end
def close
@log_actor.destroy
@lock.synchronize do
@logger.close
@closed = true
end
end
def flush
@lock.synchronize do
@logger.flush
end
end
def take_snapshot
@lock.synchronize(:SH) do
@lock.synchronize do
@logger.close
end
Snapshot.new(@directory_name, system, @marshaller).take
@logger.reset
end
end
private
def create_lock
Sync.new
end
def create_logger(directory_name, log_factory)
BatchedLogger.new(directory_name, log_factory, self.system)
end
def log_factory
BatchedLogFactory.new
end
end
private
class LogActor
def self.launch(madeleine, delay=0.01)
result = new(madeleine, delay)
result
end
def destroy
@is_destroyed = true
if @thread.alive?
@thread.wakeup
@thread.join
end
end
private
def initialize(madeleine, delay)
@is_destroyed = false
madeleine.flush
@thread = Thread.new {
until @is_destroyed
sleep(delay)
madeleine.flush
end
}
end
end
class BatchedLogFactory
def create_log(directory_name)
BatchedLog.new(directory_name)
end
end
class BatchedLogger < Logger
def initialize(directory_name, log_factory, system)
super(directory_name, log_factory)
@buffer = []
@system = system
end
def store(queued_command)
@buffer << queued_command
end
def close
return if @log.nil?
flush
@log.close
@log = nil
end
def flush
return if @buffer.empty?
open_new_log if @log.nil?
if @system.kind_of?(ClockedSystem)
@buffer.unshift(QueuedTick.new)
end
@buffer.each do |queued_command|
queued_command.store(@log)
end
@log.flush
@buffer.each do |queued_command|
queued_command.execute(@system)
end
@buffer.clear
end
end
class BatchedLog < CommandLog
def store(command)
Marshal.dump(command, @file)
end
def flush
@file.flush
@file.fsync
end
end
class QueuedCommand
def initialize(command)
@command = command
@pipe = SimplisticPipe.new
end
def store(log)
@pipe.write(log)
end
def execute(system)
@pipe.write(system)
end
def wait_for
@pipe.read do |log|
log.store(@command)
end
@pipe.read do |system|
return @command.execute(system)
end
end
end
class QueuedTick
def initialize
@tick = Tick.new(Time.now)
end
def store(log)
log.store(@tick)
end
def execute(system)
@tick.execute(system)
end
end
class SimplisticPipe
def initialize
@receive_lock = Mutex.new.lock
@consume_lock = Mutex.new.lock
@message = nil
end
def read
begin
wait_for_message_received
if block_given?
yield @message
else
return @message
end
ensure
message_consumed
end
end
def write(message)
raise WriteBlockedException unless can_write?
@message = message
message_received
wait_for_message_consumed
@message = nil
end
def can_write?
@message.nil?
end
private
def message_received
@receive_lock.unlock
end
def wait_for_message_received
@receive_lock.lock
end
def message_consumed
@consume_lock.unlock
end
def wait_for_message_consumed
@consume_lock.lock
end
end
class WriteBlockedException < Exception
end
end
end
BatchedSnapshotMadeleine = Madeleine::Batch::BatchedSnapshotMadeleine

View file

@ -1,35 +0,0 @@
#!/usr/local/bin/ruby -w
$LOAD_PATH.unshift("../lib")
require 'madeleine'
require 'batched'
class BenchmarkCommand
def initialize(value)
@value = value
end
def execute(system)
# do nothing
end
end
madeleine = BatchedSnapshotMadeleine.new("benchmark-base") { :the_system }
RUNS = 2000
GC.start
GC.disable
t0 = Time.now
RUNS.times {
madeleine.execute_command(BenchmarkCommand.new(1234))
}
t1 = Time.now
GC.enable
tps = RUNS/(t1 - t0)
puts "#{tps.to_i} transactions/s"

View file

@ -1,245 +0,0 @@
#!/usr/local/bin/ruby -w
#
# Copyright(c) 2003 Håkan Råberg
#
# Some components taken from test_persistence.rb
# Copyright(c) 2003 Anders Bengtsson
#
$LOAD_PATH.unshift("../lib")
require 'batched'
require 'test/unit'
require 'madeleine/clock'
module Madeleine::Batch
class BatchedSnapshotMadeleineTest < Test::Unit::TestCase
class ArraySystem < Array
include Madeleine::Clock::ClockedSystem
end
class PushCommand
def initialize(value)
@value = value
end
def execute(system)
system << @value
end
end
class ArrayQuery
def initialize
@time = []
end
def execute(system)
length = system.length
@time << system.clock.time
a = 1
system.each do |n|
a *= n
end
raise "inconsistent read" unless length == system.length
raise "inconsistent read" unless @time.last == system.clock.time
end
end
def test_live_snapshot
system = ArraySystem.new
w, r = [], []
going = true
madeleine = BatchedSnapshotMadeleine.new(prevalence_base) { system }
i = 0
10.times do |n|
w[n] = Thread.new {
while going
madeleine.execute_command(PushCommand.new(i))
i += 1
sleep(0.1)
end
}
end
q = 0
query = ArrayQuery.new
100.times do |n|
r[n] = Thread.new {
while going
begin
madeleine.execute_query(query)
q += 1
rescue
fail("Query blocks writing")
end
sleep(0.1)
end
}
end
s = 0
snapshot = Thread.new {
while going
madeleine.take_snapshot
s += 1
sleep(0.01)
end
}
sleep(1)
going = false
r.each do |t|
t.join
end
w.each do |t|
t.join
end
snapshot.join
madeleine.close
madeleine2 = SnapshotMadeleine.new(prevalence_base)
assert_equal(madeleine.system, madeleine2.system, "Take system snapshots while accessing")
end
def prevalence_base
"BatchedSnapshot"
end
def teardown
delete_directory(prevalence_base)
end
end
class BatchedLogTest < Test::Unit::TestCase
class MockMadeleine
def initialize(logger)
@logger = logger
end
def flush
@logger.flush
end
end
class MockCommand
attr_reader :text
def initialize(text)
@text = text
end
def execute(system)
end
def ==(o)
o.text == @text
end
end
module BufferInspector
def buffer_size
@buffer.size
end
end
def setup
@target = BatchedLogger.new(".", BatchedLogFactory.new, nil)
@target.extend(BufferInspector)
@madeleine = MockMadeleine.new(@target)
@messages = []
end
def test_logging
actor = LogActor.launch(@madeleine, 0.1)
append("Hello")
sleep(0.01)
append("World")
sleep(0.01)
assert_equal(2, @target.buffer_size, "Batched command queue")
assert(!File.exist?(expected_file_name), "Batched commands not on disk")
sleep(0.2)
assert_equal(0, @target.buffer_size, "Queue emptied by batched write")
file_size = File.size(expected_file_name)
assert(file_size > 0, "Queue written to disk")
append("Again")
sleep(0.2)
assert(File.size(expected_file_name) > file_size, "Command written to disk")
f = File.new(expected_file_name)
@messages.each do |message|
assert_equal(message, Marshal.load(f), "Commands logged in order")
end
f.close
actor.destroy
@target.flush
@target.close
end
def append(text)
Thread.new {
message = MockCommand.new(text)
@messages << message
queued_command = QueuedCommand.new(message)
@target.store(queued_command)
queued_command.wait_for
}
end
def expected_file_name
"000000000000000000001.command_log"
end
def teardown
assert(File.delete(expected_file_name) == 1)
end
end
def delete_directory(directory_name)
Dir.foreach(directory_name) do |file|
next if file == "."
next if file == ".."
assert(File.delete(directory_name + File::SEPARATOR + file) == 1,
"Unable to delete #{file}")
end
Dir.delete(directory_name)
end
end
include Madeleine::Batch
def add_batched_tests(suite)
suite << BatchedSnapshotMadeleineTest.suite
suite << BatchedLogTest.suite
end
if __FILE__ == $0
suite = Test::Unit::TestSuite.new("BatchedLogTest")
add_batched_tests(suite)
require 'test/unit/ui/console/testrunner'
Thread.abort_on_exception = true
Test::Unit::UI::Console::TestRunner.run(suite)
end

View file

@ -1,248 +0,0 @@
#!/usr/local/bin/ruby -w
#
# Copyright(c) 2003 Håkan Råberg
#
# This test is based on Prevaylers TransactionTestRun,
# Copyright(c) 2001-2003 Klaus Wuestefeld.
#
$LOAD_PATH.unshift("../lib")
require 'madeleine'
require 'madeleine/clock'
require 'batched'
module ScalabilityTest
class TransactionTestRun
MIN_THREADS = 20
MAX_THREADS = 20
NUMBER_OF_OBJECTS = 100000
ROUND_DURATION = 20
DIR = "ScalabilityBase"
def initialize
@system = TransactionSystem.new
@madeleine = BatchedSnapshotMadeleine.new(DIR) { @system }
@system.replace_all_records(create_records(NUMBER_OF_OBJECTS))
@is_round_finished = false
@best_round_ops_per_s = 0
@best_round_threads = 0
@operation_count = 0
@last_operation = 0
@active_round_threads = 0
@half_of_the_objects = NUMBER_OF_OBJECTS / 2
@connection_cache = []
@connection_cache_lock = Mutex.new
ObjectSpace.garbage_collect
puts "========= Running " + name + " (" + (MAX_THREADS - MIN_THREADS + 1).to_s + " rounds). Subject: " + subject_name + "..."
puts "Each round will take approx. " + ROUND_DURATION.to_s + " seconds to run..."
perform_test
puts "----------- BEST ROUND: " + result_string(@best_round_ops_per_s, @best_round_threads)
@madeleine.close
delete_directory(DIR)
end
def name
"Transaction Test"
end
def subject_name
"Madeleine"
end
def result_string(ops_per_s, threads)
ops_per_s.to_s + " operations/second (" + threads.to_s + " threads)"
end
def perform_test
for threads in MIN_THREADS..MAX_THREADS
ops_per_s = perform_round(threads)
if ops_per_s > @best_round_ops_per_s
@best_round_ops_per_s = ops_per_s
@best_round_threads = threads
end
end
end
def perform_round(threads)
initial_operation_count = @operation_count
start_time = Time.now.to_f
start_threads(threads)
sleep(ROUND_DURATION)
stop_threads
seconds_ellapsed = Time.now.to_f - start_time
ops_per_second = (@operation_count - initial_operation_count) / seconds_ellapsed
puts
puts "Seconds ellapsed: " + seconds_ellapsed.to_s
puts "--------- Round Result: " + result_string(ops_per_second, threads)
ops_per_second
end
def start_threads(threads)
@is_round_finished = false
for i in 1..threads
start_thread(@last_operation + i, threads)
end
end
def start_thread(starting_operation, operation_increment)
Thread.new {
connection = accquire_connection
operation = starting_operation
while not @is_round_finished
# puts "Operation " + operation.to_s
execute_operation(connection, operation)
operation += operation_increment
end
@connection_cache_lock.synchronize do
@connection_cache << connection
@operation_count += (operation - starting_operation) / operation_increment
@last_operation = operation if @last_operation < operation
@active_round_threads -= 1
end
}
@active_round_threads += 1
end
def execute_operation(connection, operation)
record_to_insert = Record.new(NUMBER_OF_OBJECTS + operation)
id_to_delete = spread_id(operation)
record_to_update = Record.new(@half_of_the_objects + id_to_delete)
connection.perform_transaction(record_to_insert, record_to_update, id_to_delete)
end
def spread_id(id)
(id / @half_of_the_objects) * @half_of_the_objects + ((id * 16807) % @half_of_the_objects)
end
def create_test_connection
TransactionConnection.new(@madeleine)
end
def accquire_connection
@connection_cache_lock.synchronize do
return @connection_cache.empty? ? create_test_connection : @connection_cache.shift
end
end
def stop_threads
@is_round_finished = true
while @active_round_threads != 0
sleep(0.001)
end
end
def create_records(number_of_objects)
result = []
for i in 0..number_of_objects
result << Record.new(i)
end
result
end
def delete_directory(directory_name)
Dir.foreach(directory_name) do |file|
next if file == "."
next if file == ".."
File.delete(directory_name + File::SEPARATOR + file)
end
Dir.delete(directory_name)
end
end
class TransactionSystem
include Madeleine::Clock::ClockedSystem
def initialize
@records_by_id = Hash.new
@transaction_lock = Mutex.new
end
def perform_transaction(record_to_insert, record_to_update, id_to_delete)
@transaction_lock.synchronize do
put(record_to_insert)
put(record_to_update)
@records_by_id.delete(id_to_delete)
end
end
def put(new_record)
@records_by_id[new_record.id] = new_record
end
def replace_all_records(new_records)
@records_by_id.clear
new_records.each do |record|
put(record)
end
end
end
class TransactionConnection
def initialize(madeleine)
@madeleine = madeleine
end
def perform_transaction(record_to_insert, record_to_update, id_to_delete)
@madeleine.execute_command(TestTransaction.new(record_to_insert, record_to_update, id_to_delete))
end
end
class TestTransaction
def initialize(record_to_insert, record_to_update, id_to_delete)
@record_to_insert = record_to_insert
@record_to_update = record_to_update
@id_to_delete = id_to_delete
end
def execute(system)
system.perform_transaction(@record_to_insert, @record_to_update, @id_to_delete)
end
end
class Record
attr_reader :id, :name, :string_1, :date_1, :date_2
def initialize(id)
@id = id
@name = "NAME" + (id % 10000).to_s
@string_1 = (id % 10000).to_s == 0 ? Record.large_string + id : nil;
@date_1 = Record.random_date
@date_2 = Record.random_date
end
def self.large_string
[].fill("A", 1..980).to_s
end
def self.random_date
rand(10000000)
end
end
end
if __FILE__ == $0
puts "Madeleine Scalability Test"
puts "Based on Prevaylers Scalability Test"
puts
Thread.abort_on_exception = true
ScalabilityTest::TransactionTestRun.new
end

View file

@ -1,44 +0,0 @@
#!/usr/local/bin/ruby -w
$LOAD_PATH.unshift("../lib")
require 'madeleine'
require 'batched'
class BenchmarkCommand
def initialize(value)
@value = value
end
def execute(system)
# do nothing
end
end
madeleine = BattchedSnapshotMadeleine.new("benchmark-base") { :the_system }
RUNS = 200
THREADS = 10
GC.start
GC.disable
t0 = Time.now
threads = []
THREADS.times {
threads << Thread.new {
RUNS.times {
madeleine.execute_command(BenchmarkCommand.new(1234))
}
}
}
threads.each {|t| t.join }
t1 = Time.now
GC.enable
tps = (THREADS * RUNS)/(t1 - t0)
puts "#{tps.to_i} transactions/s"

View file

@ -1 +0,0 @@
api

View file

@ -1,87 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<title>Design rules - Madeleine</title>
<link type="text/css" href="docs.css" rel="stylesheet">
</head>
<body>
<h1>Design rules</h1>
<p>This is a summary of the design rules your application has to
follow to work with Madeleine.
<h2>The Prevalent System</h2>
<h3>Your objects have to fit into memory</h3>
<p>All of them. At the same time.
<h3>Your objects have to be marshallable</h3>
<p>Snapshots are taken of the system by marshalling the whole system to a
file. If your classes can't be marshalled/unmarshalled then Madeleine
won't be able to store/restore the system.
<h3>Your objects have to be deterministic</h3>
<p><em>Deterministic</em> means that, given the same commands, they have
to always give the same results.
<p>For the much of your code this won't
be a problem, but there are a few common issues:
<h4>The system clock</h4>
<p>You can't use the system clock (see instead ClockedSystem and TimeActor).
<h4>Random numbers</h4>
<p><code>Kernel.rand()</code> uses the system clock internally by
default. Use <code>Kernel.srand()</code> to seed the random number
generator before using <code>rand()</code>.
<h4>Files, network and other IO</h4>
<p>You generally can't access the outside world from within your
prevalent system. Instead do IO outside of the prevalent system and
call into the system when needed.
<h3>Changes to the system have to be done through command
objects</h3>
<p>Everything that modifies the prevalent system must be done through a
<em>command object</em> sent to the Madeleine instance, using
<code>execute_command(aCommand)</code>. Queries that don't modify the
system can be done either through direct method calls or through
command objects.
<h2>Command Objects</h2>
<p>A command object is an object that implements the method
<code>execute(system)</code>. They are an example of the "Command"
design pattern.
<h3>The command objects also have to be marshallable</h3>
<p>Madeleine keeps track of changes between snapshots by logging
marshalled commands.
<h3>The command must raise errors before modifying the system</h3>
<p>Unlike a RDBMS, Madeleine can't roll back a command (yet). This means
that your commands will have to do their error checking and raise any
errors before modifying the system. Failing to do this will cause an
inconsistent command log.
<h3>Command objects can't hold references to the system's objects</h3>
<p>Unmarshalling such a command would create clones of the original
objects, which would then be modified instead of the real
objects. The commands must <i>find</i> the objects to modify.
<hr>
<tt>$Id: designRules.html,v 1.1 2005/01/07 23:03:27 alexeyv Exp $</tt>
</body>
</html>

View file

@ -1,28 +0,0 @@
body {
background-color: #FFFFF0;
}
p {
width: 70ex
}
h1 {
font-family: verdana,arial,helvetica,sans-serif;
}
h2 {
font-family: verdana,arial,helvetica,sans-serif;
background: #EEEEE0;
}
h3 {
font-family: verdana,arial,helvetica,sans-serif;
}
h4 {
font-family: verdana,arial,helvetica,sans-serif;
}
.classMethod {
font-family: courier,monospace;
font-weight: bold;
background: #EEEEE0;
}
.instanceMethod {
font-family: courier,sans-serif;
background: #EEEEE0;
}

View file

@ -1,3 +0,0 @@
#!/usr/local/bin/ruby
`rdoc lib --op docs/api`

File diff suppressed because it is too large Load diff

View file

@ -1,420 +0,0 @@
#
# Madeleine - Ruby Object Prevalence
#
# Author:: Anders Bengtsson <ndrsbngtssn@yahoo.se>
# Copyright:: Copyright (c) 2003-2004
#
# Usage:
#
# require 'madeleine'
#
# madeleine = SnapshotMadeleine.new("my_example_storage") {
# SomeExampleApplication.new()
# }
#
# madeleine.execute_command(command)
#
module Madeleine
require 'thread'
require 'sync'
require 'madeleine/files'
MADELEINE_VERSION = "0.7.1"
class SnapshotMadeleine
# Builds a new Madeleine instance. If there is a snapshot available
# then the system will be created from that, otherwise
# <tt>new_system</tt> will be used. The state of the system will
# then be restored from the command logs.
#
# You can provide your own snapshot marshaller, for instance using
# YAML or SOAP, instead of Ruby's built-in marshaller. The
# <tt>snapshot_marshaller</tt> must respond to
# <tt>load(stream)</tt> and <tt>dump(object, stream)</tt>. You
# must use the same marshaller every time for a system.
#
# See: DefaultSnapshotMadeleine
#
# * <tt>directory_name</tt> - Storage directory to use. Will be created if needed.
# * <tt>snapshot_marshaller</tt> - Marshaller to use for system snapshots. (Optional)
# * <tt>new_system_block</tt> - Block to create a new system (if no stored system was found).
def self.new(directory_name, snapshot_marshaller=Marshal, &new_system_block)
log_factory = DefaultLogFactory.new
logger = Logger.new(directory_name,
log_factory)
snapshotter = Snapshotter.new(directory_name,
snapshot_marshaller)
lock = DefaultLock.new
recoverer = Recoverer.new(directory_name,
snapshot_marshaller)
system = recoverer.recover_snapshot(new_system_block)
executer = Executer.new(system)
recoverer.recover_logs(executer)
DefaultSnapshotMadeleine.new(system, logger, snapshotter, lock, executer)
end
end
class DefaultSnapshotMadeleine
# The prevalent system
attr_reader :system
def initialize(system, logger, snapshotter, lock, executer)
@system = system
@logger = logger
@snapshotter = snapshotter
@lock = lock
@executer = executer
@closed = false
end
# Execute a command on the prevalent system.
#
# Commands must have a method <tt>execute(aSystem)</tt>.
# Otherwise an error, <tt>Madeleine::InvalidCommandException</tt>,
# will be raised.
#
# The return value from the command's <tt>execute()</tt> method is returned.
#
# * <tt>command</tt> - The command to execute on the system.
def execute_command(command)
verify_command_sane(command)
@lock.synchronize {
raise "closed" if @closed
@logger.store(command)
@executer.execute(command)
}
end
# Execute a query on the prevalent system.
#
# Only differs from <tt>execute_command</tt> in that the command/query isn't logged, and
# therefore isn't allowed to modify the system. A shared lock is held, preventing others
# from modifying the system while the query is running.
#
# * <tt>query</tt> - The query command to execute
def execute_query(query)
@lock.synchronize_shared {
@executer.execute(query)
}
end
# Take a snapshot of the current system.
#
# You need to regularly take a snapshot of a running system,
# otherwise the logs will grow big and restarting the system will take a
# long time. Your backups must also be done from the snapshot files,
# since you can't make a consistent backup of a live log.
#
# A practical way of doing snapshots is a timer thread:
#
# Thread.new(madeleine) {|madeleine|
# while true
# sleep(60 * 60 * 24) # 24 hours
# madeleine.take_snapshot
# end
# }
def take_snapshot
@lock.synchronize {
@logger.close
@snapshotter.take(@system)
@logger.reset
}
end
# Close the system.
#
# The log file is closed and no new commands can be received
# by this Madeleine.
def close
@lock.synchronize {
@logger.close
@closed = true
}
end
private
def verify_command_sane(command)
unless command.respond_to?(:execute)
raise InvalidCommandException.new("Commands must have an 'execute' method")
end
end
end
class InvalidCommandException < Exception
end
#
# Internal classes below
#
FILE_COUNTER_SIZE = 21 #:nodoc:
class DefaultLock #:nodoc:
def initialize
@lock = Sync.new
end
def synchronize(&block)
@lock.synchronize(&block)
end
def synchronize_shared(&block)
@lock.synchronize(:SH, &block)
end
end
class Executer #:nodoc:
def initialize(system)
@system = system
@in_recovery = false
end
def execute(command)
begin
command.execute(@system)
rescue
raise unless @in_recovery
end
end
def recovery
begin
@in_recovery = true
yield
ensure
@in_recovery = false
end
end
end
class Recoverer #:nodoc:
def initialize(directory_name, marshaller)
@directory_name, @marshaller = directory_name, marshaller
end
def recover_snapshot(new_system_block)
system = nil
id = SnapshotFile.highest_id(@directory_name)
if id > 0
snapshot_file = SnapshotFile.new(@directory_name, id).name
open(snapshot_file, "rb") {|snapshot|
system = @marshaller.load(snapshot)
}
else
system = new_system_block.call
end
system
end
def recover_logs(executer)
executer.recovery {
CommandLog.log_file_names(@directory_name, FileService.new).each {|file_name|
open(@directory_name + File::SEPARATOR + file_name, "rb") {|log|
recover_log(executer, log)
}
}
}
end
private
def recover_log(executer, log)
while ! log.eof?
command = Marshal.load(log)
executer.execute(command)
end
end
end
class NumberedFile #:nodoc:
def initialize(path, name, id)
@path, @name, @id = path, name, id
end
def name
result = @path
result += File::SEPARATOR
result += sprintf("%0#{FILE_COUNTER_SIZE}d", @id)
result += '.'
result += @name
end
end
class CommandLog #:nodoc:
def self.log_file_names(directory_name, file_service)
return [] unless file_service.exist?(directory_name)
result = file_service.dir_entries(directory_name).select {|name|
name =~ /^\d{#{FILE_COUNTER_SIZE}}\.command_log$/
}
result.each {|name| name.untaint }
result.sort!
result
end
def initialize(path, file_service)
id = self.class.highest_log(path, file_service) + 1
numbered_file = NumberedFile.new(path, "command_log", id)
@file = file_service.open(numbered_file.name, 'wb')
end
def close
@file.close
end
def store(command)
Marshal.dump(command, @file)
@file.flush
@file.fsync
end
def self.highest_log(directory_name, file_service)
highest = 0
log_file_names(directory_name, file_service).each {|file_name|
match = /^(\d{#{FILE_COUNTER_SIZE}})/.match(file_name)
n = match[1].to_i
if n > highest
highest = n
end
}
highest
end
end
class DefaultLogFactory #:nodoc:
def create_log(directory_name)
CommandLog.new(directory_name, FileService.new)
end
end
class Logger #:nodoc:
def initialize(directory_name, log_factory)
@directory_name = directory_name
@log_factory = log_factory
@log = nil
@pending_tick = nil
ensure_directory_exists
end
def ensure_directory_exists
if ! File.exist?(@directory_name)
Dir.mkdir(@directory_name)
end
end
def reset
close
delete_log_files
end
def store(command)
if command.kind_of?(Madeleine::Clock::Tick)
@pending_tick = command
else
if @pending_tick
internal_store(@pending_tick)
@pending_tick = nil
end
internal_store(command)
end
end
def internal_store(command)
if @log.nil?
open_new_log
end
@log.store(command)
end
def close
return if @log.nil?
@log.close
@log = nil
end
private
def delete_log_files
Dir.glob(@directory_name + File::SEPARATOR + "*.command_log").each {|name|
name.untaint
File.delete(name)
}
end
def open_new_log
@log = @log_factory.create_log(@directory_name)
end
end
class SnapshotFile < NumberedFile #:nodoc:
def self.highest_id(directory_name)
return 0 unless File.exist?(directory_name)
suffix = "snapshot"
highest = 0
Dir.foreach(directory_name) {|file_name|
match = /^(\d{#{FILE_COUNTER_SIZE}}\.#{suffix}$)/.match(file_name)
next unless match
n = match[1].to_i
if n > highest
highest = n
end
}
highest
end
def self.next(directory_name)
new(directory_name, highest_id(directory_name) + 1)
end
def initialize(directory_name, id)
super(directory_name, "snapshot", id)
end
end
class Snapshotter #:nodoc:
def initialize(directory_name, marshaller)
@directory_name, @marshaller = directory_name, marshaller
end
def take(system)
numbered_file = SnapshotFile.next(@directory_name)
name = numbered_file.name
open(name + '.tmp', 'wb') {|snapshot|
@marshaller.dump(system, snapshot)
snapshot.flush
snapshot.fsync
}
File.rename(name + '.tmp', name)
end
end
module Clock #:nodoc:
class Tick #:nodoc:
def initialize(time)
@time = time
end
def execute(system)
system.clock.forward_to(@time)
end
end
end
end
SnapshotMadeleine = Madeleine::SnapshotMadeleine

View file

@ -1,418 +0,0 @@
require 'yaml'
require 'madeleine/zmarshal'
require 'soap/marshal'
module Madeleine
# Automatic commands for Madeleine
#
# Author:: Stephen Sykes <sds@stephensykes.com>
# Copyright:: Copyright (C) 2003-2004
# Version:: 0.41
#
# This module provides a way of automatically generating command objects for madeleine to
# store. It works by making a proxy object for all objects of any classes in which it is included.
# Method calls to these objects are intercepted, and stored as a command before being
# passed on to the real receiver. The command objects remember which object the command was
# destined for by using a pair of internal ids that are contained in each of the proxy objects.
#
# There is also a mechanism for specifying which methods not to intercept calls to by using
# automatic_read_only, and its opposite automatic_read_write.
#
# Should you require it, the snapshots can be stored as yaml, and can be compressed. Just pass
# the marshaller you want to use as the second argument to AutomaticSnapshotMadeleine.new.
# If the passed marshaller did not successfully deserialize the latest snapshot, the system
# will try to automatically detect and read either Marshal, YAML, SOAP, or their corresponding
# compressed versions.
#
# This module is designed to work correctly in the case there are multiple madeleine systems in use by
# a single program, and is also safe to use with threads.
#
# Usage:
#
# require 'madeleine'
# require 'madeleine/automatic'
#
# class A
# include Madeleine::Automatic::Interceptor
# attr_reader :foo
# automatic_read_only :foo
# def initialize(param1, ...)
# ...
# end
# def some_method(paramA, ...)
# ...
# end
# automatic_read_only
# def bigfoo
# foo.upcase
# end
# end
#
# mad = AutomaticSnapshotMadeleine.new("storage_directory") { A.new(param1, ...) }
#
# mad.system.some_method(paramA, ...) # logged as a command by madeleine
# print mad.foo # not logged
# print mad.bigfoo # not logged
# mad.take_snapshot
#
module Automatic
#
# This module should be included (at the top) in any classes that are to be persisted.
# It will intercept method calls and make sure they are converted into commands that are logged by Madeleine.
# It does this by returning a Prox object that is a proxy for the real object.
#
# It also handles automatic_read_only and automatic_read_write, allowing user specification of which methods
# should be made into commands
#
module Interceptor
#
# When included, redefine new so that we can return a Prox object instead, and define methods to handle
# keeping track of which methods are read only
#
def self.included(klass)
class <<klass
alias_method :_old_new, :new
def new(*args, &block)
Prox.new(_old_new(*args, &block))
end
#
# Called when a method added - remember symbol if read only
# This is a good place to add in any superclass's read only methods also
#
def method_added(symbol)
self.instance_eval {
@read_only_methods ||= []
@auto_read_only_flag ||= false
@read_only_methods << symbol if @auto_read_only_flag
c = self
while (c = c.superclass)
if (c.instance_eval {instance_variables.include? "@read_only_methods"})
@read_only_methods |= c.instance_eval {@read_only_methods}
end
end
}
end
#
# Set the read only flag, or add read only methods
#
def automatic_read_only(*list)
if (list == [])
self.instance_eval {@auto_read_only_flag = true}
else
list.each {|s| self.instance_eval {@read_only_methods ||= []; @read_only_methods << s}}
end
end
#
# Clear the read only flag, or remove read only methods
#
def automatic_read_write(*list)
if (list == [])
self.instance_eval {@auto_read_only_flag = false}
else
list.each {|s| self.instance_eval {@read_only_methods ||= []; @read_only_methods.delete(s)}}
end
end
end
end
#
# Return the list of read only methods so Automatic_proxy#method_missing can find what to and what not to make into a command
#
def read_only_methods
self.class.instance_eval {@read_only_methods}
end
end
#
# A Command object is automatically created for each method call to an object within the system that comes from without.
# These objects are recorded in the log by Madeleine.
#
class Command
def initialize(symbol, myid, *args)
@symbol = symbol
@myid = myid
@args = args
end
#
# Called by madeleine when the command is done either first time, or when restoring the log
#
def execute(system)
Thread.current[:system].myid2ref(@myid).thing.send(@symbol, *@args)
end
end
#
# This is a little class to pass to SnapshotMadeleine. This is used for snapshots only.
# It acts as the marshaller, and just passes marshalling requests on to the user specified
# marshaller. This defaults to Marshal, but could be YAML or another.
# After we have done a restore, the ObjectSpace is searched for instances of Prox to
# add new objects to the list in AutomaticSnapshotMadeleine
#
class Automatic_marshaller #:nodoc:
def Automatic_marshaller.load(io)
restored_obj = Deserialize.load(io, Thread.current[:system].marshaller)
ObjectSpace.each_object(Prox) {|o| Thread.current[:system].restore(o) if (o.sysid == restored_obj.sysid)}
restored_obj
end
def Automatic_marshaller.dump(obj, io = nil)
Thread.current[:system].marshaller.dump(obj, io)
end
end
#
# A Prox object is generated and returned by Interceptor each time a system object is created.
#
class Prox #:nodoc:
attr_accessor :thing, :myid, :sysid
def initialize(thing)
if (thing)
raise "App object created outside of app" unless Thread.current[:system]
@sysid = Thread.current[:system].sysid
@myid = Thread.current[:system].add(self)
@thing = thing
end
end
#
# This automatically makes and executes a new Command if a method is called from
# outside the system.
#
def method_missing(symbol, *args, &block)
# print "Sending #{symbol} to #{@thing.to_s}, myid=#{@myid}, sysid=#{@sysid}\n"
raise NoMethodError, "Undefined method" unless @thing.respond_to?(symbol)
if (Thread.current[:system])
@thing.send(symbol, *args, &block)
else
raise "Cannot make command with block" if block_given?
Thread.current[:system] = AutomaticSnapshotMadeleine.systems[@sysid]
begin
if (@thing.read_only_methods.include?(symbol))
result = Thread.current[:system].execute_query(Command.new(symbol, @myid, *args))
else
result = Thread.current[:system].execute_command(Command.new(symbol, @myid, *args))
end
ensure
Thread.current[:system] = false
end
result
end
end
#
# Custom marshalling - this adds the internal id (myid) and the system id to a marshal
# of the object we are the proxy for.
# We take care to not marshal the same object twice, so circular references will work.
# We ignore Thread.current[:system].marshaller here - this is only called by Marshal, and
# marshal is always used for Command objects
#
def _dump(depth)
if (Thread.current[:snapshot_memory])
if (Thread.current[:snapshot_memory][self])
[@myid.to_s, @sysid].pack("A8A30")
else
Thread.current[:snapshot_memory][self] = true
[@myid.to_s, @sysid].pack("A8A30") + Marshal.dump(@thing, depth)
end
else
[@myid.to_s, @sysid].pack("A8A30") # never marshal a prox object in a command, just ref
end
end
#
# Custom marshalling for Marshal - restore a Prox object.
#
def Prox._load(str)
x = Prox.new(nil)
a = str.unpack("A8A30a*")
x.myid = a[0].to_i
x.sysid = a[1]
x = Thread.current[:system].restore(x)
x.thing = Marshal.load(a[2]) if (a[2] > "")
x
end
end
#
# The AutomaticSnapshotMadeleine class contains an instance of the persister
# (default is SnapshotMadeleine) and provides additional automatic functionality.
#
# The class is instantiated the same way as SnapshotMadeleine:
# madeleine_sys = AutomaticSnapshotMadeleine.new("storage_directory") { A.new(param1, ...) }
# The second initialisation parameter is the persister. Supported persisters are:
#
# * Marshal (default)
# * YAML
# * SOAP::Marshal
# * Madeleine::ZMarshal.new(Marshal)
# * Madeleine::ZMarshal.new(YAML)
# * Madeleine::ZMarshal.new(SOAP::Marshal)
#
# The class keeps a record of all the systems that currently exist.
# Each instance of the class keeps a record of Prox objects in that system by internal id (myid).
#
# We also add functionality to take_snapshot in order to set things up so that the custom Prox object
# marshalling will work correctly.
#
class AutomaticSnapshotMadeleine
attr_accessor :marshaller
attr_reader :list, :sysid
def initialize(directory_name, marshaller=Marshal, persister=SnapshotMadeleine, &new_system_block)
@sysid ||= Time.now.to_f.to_s + Thread.current.object_id.to_s # Gererate a new sysid
@myid_count = 0
@list = {}
Thread.current[:system] = self # during system startup system should not create commands
Thread.critical = true
@@systems ||= {} # holds systems by sysid
@@systems[@sysid] = self
Thread.critical = false
@marshaller = marshaller # until attrb
begin
@persister = persister.new(directory_name, Automatic_marshaller, &new_system_block)
@list.delete_if {|k,v| # set all the prox objects that now exist to have the right sysid
begin
ObjectSpace._id2ref(v).sysid = @sysid
false
rescue RangeError
true # Id was to a GC'd object, delete it
end
}
ensure
Thread.current[:system] = false
end
end
#
# Add a proxy object to the list, return the myid for that object
#
def add(proxo)
@list[@myid_count += 1] = proxo.object_id
@myid_count
end
#
# Restore a marshalled proxy object to list - myid_count is increased as required.
# If the object already exists in the system then the existing object must be used.
#
def restore(proxo)
if (@list[proxo.myid])
proxo = myid2ref(proxo.myid)
else
@list[proxo.myid] = proxo.object_id
@myid_count = proxo.myid if (@myid_count < proxo.myid)
end
proxo
end
#
# Returns a reference to the object indicated by the internal id supplied.
#
def myid2ref(myid)
raise "Internal id #{myid} not found" unless objid = @list[myid]
ObjectSpace._id2ref(objid)
end
#
# Take a snapshot of the system.
#
def take_snapshot
begin
Thread.current[:system] = self
Thread.current[:snapshot_memory] = {}
@persister.take_snapshot
ensure
Thread.current[:snapshot_memory] = nil
Thread.current[:system] = false
end
end
#
# Returns the hash containing the systems.
#
def AutomaticSnapshotMadeleine.systems
@@systems
end
#
# Close method changes the sysid for Prox objects so they can't be mistaken for real ones in a new
# system before GC gets them
#
def close
begin
@list.each_key {|k| myid2ref(k).sysid = nil}
rescue RangeError
# do nothing
end
@persister.close
end
#
# Pass on any other calls to the persister
#
def method_missing(symbol, *args, &block)
@persister.send(symbol, *args, &block)
end
end
module Deserialize #:nodoc:
#
# Detect format of an io stream. Leave it rewound.
#
def Deserialize.detect(io)
c = io.getc
c1 = io.getc
io.rewind
if (c == Marshal::MAJOR_VERSION && c1 <= Marshal::MINOR_VERSION)
Marshal
elsif (c == 31 && c1 == 139) # gzip magic numbers
ZMarshal
else
while (s = io.gets)
break if (s !~ /^\s*$/) # ignore blank lines
end
io.rewind
if (s && s =~ /^\s*<\?[xX][mM][lL]/) # "<?xml" begins an xml serialization
SOAP::Marshal
else
while (s = io.gets)
break if (s !~ /^\s*#/ && s !~ /^\s*$/) # ignore blank and comment lines
end
io.rewind
if (s && s =~ /^\s*---/) # "---" is the yaml header
YAML
else
nil # failed to detect
end
end
end
end
#
# Try to deserialize object. If there was an error, try to detect marshal format,
# and return deserialized object using the right marshaller
# If detection didn't work, raise up the exception
#
def Deserialize.load(io, marshaller=Marshal)
begin
marshaller.load(io)
rescue Exception => e
io.rewind
detected_marshaller = detect(io)
if (detected_marshaller == ZMarshal)
zio = Zlib::GzipReader.new(io)
detected_zmarshaller = detect(zio)
zio.finish
io.rewind
if (detected_zmarshaller)
ZMarshal.new(detected_zmarshaller).load(io)
else
raise e
end
elsif (detected_marshaller)
detected_marshaller.load(io)
else
raise e
end
end
end
end
end
end
AutomaticSnapshotMadeleine = Madeleine::Automatic::AutomaticSnapshotMadeleine

View file

@ -1,94 +0,0 @@
#
# Copyright(c) Anders Bengtsson 2003
#
require 'madeleine'
module Madeleine
module Clock
# Deprecated. Use SnapshotMadeleine instead.
class ClockedSnapshotMadeleine < ::Madeleine::SnapshotMadeleine # :nodoc:
end
# Let your system extend this module if you need to access the
# machine time. Used together with a TimeActor that keeps
# the clock current.
module ClockedSystem
# Returns this system's Clock.
def clock
unless defined? @clock
@clock = Clock.new
end
@clock
end
end
# Sends clock ticks to update a ClockedSystem, so that time can be
# dealt with in a deterministic way.
class TimeActor
# Create and launch a new TimeActor
#
# * <tt>madeleine</tt> - The SnapshotMadeleine instance to work on.
# * <tt>delay</tt> - Delay between ticks in seconds (Optional).
def self.launch(madeleine, delay=0.1)
result = new(madeleine, delay)
result
end
# Stops the TimeActor.
def destroy
@is_destroyed = true
@thread.wakeup
@thread.join
end
private_class_method :new
private
def initialize(madeleine, delay) #:nodoc:
@madeleine = madeleine
@is_destroyed = false
send_tick
@thread = Thread.new {
until @is_destroyed
sleep(delay)
send_tick
end
}
end
def send_tick
@madeleine.execute_command(Tick.new(Time.now))
end
end
# Keeps track of time in a ClockedSystem.
class Clock
# Returns the system's time as a Ruby <tt>Time</tt>.
attr_reader :time
def initialize
@time = Time.at(0)
end
def forward_to(newTime)
@time = newTime
end
end
#
# Internal classes below
#
# Deprecated. Merged into default implementation.
class TimeOptimizingLogger < ::Madeleine::Logger # :nodoc:
end
end
end
ClockedSnapshotMadeleine = Madeleine::Clock::ClockedSnapshotMadeleine

View file

@ -1,19 +0,0 @@
#
# Wrapper for Ruby's file services, replaced during testing
# so we can run tests without touching a real filesystem.
#
class FileService
def open(*args)
super(*args)
end
def exist?(name)
File.exist?(name)
end
def dir_entries(name)
Dir.entries(name)
end
end

View file

@ -1,60 +0,0 @@
#
# Author:: Anders Bengtsson <ndrsbngtssn@yahoo.se>
# Copyright:: Copyright (c) 2004
#
require 'zlib'
module Madeleine
#
# Snapshot marshaller for compressed snapshots.
#
# Compresses the snapshots created by another marshaller. Uses either
# Marshal (the default) or another supplied marshaller.
#
# Uses <tt>zlib</tt> to do on-the-fly compression/decompression.
#
# ZMarshal works with Ruby's own Marshal and YAML, but not with SOAP
# marshalling.
#
# Usage:
#
# require 'madeleine'
# require 'madeleine/zmarshal'
#
# marshaller = Madeleine::ZMarshal.new(YAML)
# madeleine = SnapshotMadeleine.new("my_example_storage", marshaller) {
# SomeExampleApplication.new()
# }
#
class ZMarshal
def initialize(marshaller=Marshal)
@marshaller = marshaller
end
def load(stream)
zstream = Zlib::GzipReader.new(stream)
begin
# Buffer into a string first, since GzipReader can't handle
# Marshal's 0-sized reads and SOAP can't handle streams at all.
# In a bright future we can revert to reading directly from the
# stream again.
buffer = zstream.read
return @marshaller.load(buffer)
ensure
zstream.finish
end
end
def dump(system, stream)
zstream = Zlib::GzipWriter.new(stream)
begin
@marshaller.dump(system, zstream)
ensure
zstream.finish
end
nil
end
end
end

View file

@ -1,23 +0,0 @@
require 'rubygems'
spec = Gem::Specification.new do |s|
s.name = 'madeleine'
s.version = '0.7.1'
s.platform = Gem::Platform::RUBY
s.required_ruby_version = ">= 1.8.1"
s.summary = "Madeleine is a Ruby implementation of Object Prevalence"
s.require_path = 'lib'
s.autorequire = 'madeleine'
s.author = "Anders Bengtsson"
s.email = "ndrsbngtssn@yahoo.se"
s.homepage = "http://madeleine.sourceforge.net"
s.files = Dir.glob("lib/**/*.rb")
s.files += Dir.glob("samples/**/*.rb")
s.files += Dir.glob("contrib/**/*.rb")
s.files += ['README', 'NEWS', 'COPYING']
end
if $0 == __FILE__
Gem::Builder.new(spec).build
end

View file

@ -1,3 +0,0 @@
dictionary-base
painter-demo
clock-demo

View file

@ -1,73 +0,0 @@
#
# Simple example of using time with Madeleine.
#
$LOAD_PATH.unshift(".." + File::SEPARATOR + "lib")
require 'madeleine/clock'
require 'tk'
# The Clicker keeps track of when it was last clicked.
#
# To access the time it extends ClockedSystem, which provides
# it with the 'clock' attribute.
#
class Clicker
include Madeleine::Clock::ClockedSystem
def initialize
@last_clicked = nil
end
def click
@last_clicked = clock.time
end
def last_clicked
return '-' if @last_clicked.nil?
@last_clicked.to_s
end
end
# A command to update the Clicker with.
#
class Click
def execute(system)
system.click
end
end
# Launch a ClockedSnapshotMadeleine.
#
# ClockedSnapshotMadeleine works like the regular SnapshotMadeleine, but
# optimizes away redundant commands from TimeActor.
#
madeleine = ClockedSnapshotMadeleine.new("clock-demo") { Clicker.new }
# Launch the TimeActor.
#
# This provides time commands, without which the system's time would stand still.
#
Madeleine::Clock::TimeActor.launch(madeleine)
clicker = madeleine.system
# The GUI
root = TkRoot.new() { title "Madeleine Clock Example" }
label = TkLabel.new(root) {
text "Last clicked " + clicker.last_clicked
width 40
pack
}
button = TkButton.new(root) {
text 'Click'
command proc {
madeleine.execute_command(Click.new)
label.text("Last clicked " + clicker.last_clicked)
}
pack
}
Tk.mainloop

View file

@ -1,23 +0,0 @@
#
# Dictionary client
#
# See dictionary_server.rb for details
#
require 'drb'
DRb.start_service
dictionary = DRbObject.new(nil, "druby://localhost:1234")
if ARGV.length == 1
puts dictionary.lookup(ARGV[0])
elsif ARGV.length == 2
dictionary.add(ARGV[0], ARGV[1])
puts "Stored"
else
puts "Usage: dictionary_client <key> [<value>]"
end

View file

@ -1,94 +0,0 @@
#
# A dictionary server using Distributed Ruby (DRb).
#
# All modifications to the dictionary are done as commands,
# while read-only queries (i.e 'lookup') are done directly.
#
# First launch this server in the background, then use
# dictionary_client.rb to look up and add items to the
# dictionary.
# You can kill the server at any time. The contents of the
# dictionary will still be there when you restart it.
#
# DRb is available at http://raa.ruby-lang.org/list.rhtml?name=druby
#
$LOAD_PATH.unshift(".." + File::SEPARATOR + "lib")
require 'madeleine'
require 'drb'
class Dictionary
def initialize
@data = {}
end
def add(key, value)
@data[key] = value
end
def lookup(key)
@data[key]
end
end
class Addition
def initialize(key, value)
@key, @value = key, value
end
def execute(system)
system.add(@key, @value)
end
end
class Lookup
def initialize(key)
@key = key
end
def execute(system)
system.lookup(@key)
end
end
class DictionaryServer
def initialize(madeleine)
@madeleine = madeleine
@dictionary = madeleine.system
end
def add(key, value)
# When adding a new key-value pair we modify the system, so
# this operation has to be done through a command.
@madeleine.execute_command(Addition.new(key, value))
end
def lookup(key)
# A lookup is a read-only operation, so we can do it as a non-logged
# query. If we weren't worried about concurrency problems we could
# have just called @dictionary.lookup(key) directly instead.
@madeleine.execute_query(Lookup.new(key))
end
end
madeleine = SnapshotMadeleine.new("dictionary-base") { Dictionary.new }
Thread.new(madeleine) {
puts "Taking snapshot every 30 seconds."
while true
sleep(30)
madeleine.take_snapshot
end
}
DRb.start_service("druby://localhost:1234",
DictionaryServer.new(madeleine))
DRb.thread.join

View file

@ -1,60 +0,0 @@
#
# Simple drawing program to show Madeleine's logging feature.
#
# When you restart the program, your old artwork is still there.
#
# (Note: The GUI components used here aren't marshal-able,
# so in a real app you would have to do custom marshaling for
# the Painter class to get working snapshots. Then again, in a real
# app you wouldn't use the GUI components to hold the app's data,
# would you?)
#
$LOAD_PATH.unshift(".." + File::SEPARATOR + "lib")
require 'madeleine'
require 'tkclass'
class Painter
def initialize(canvas)
@canvas = canvas
end
def draw(x, y)
line = Line.new(@canvas, x, y, x + 1, y + 1)
line.fill('black')
end
end
class PaintCommand
def initialize(x, y)
@x, @y = x, y
end
def execute(system)
system.draw(@x, @y)
end
end
root = TkRoot.new() { title "Madeleine Painter" }
canvas = Canvas.new(root)
canvas.pack
$madeleine = Madeleine::SnapshotMadeleine.new("painter-demo") { Painter.new(canvas) }
canvas.bind("1",
proc {|x, y|
$madeleine.execute_command(PaintCommand.new(x, y))
},
"%x %y")
canvas.bind("B1-Motion",
proc {|x, y|
$madeleine.execute_command(PaintCommand.new(x, y))
},
"%x %y")
Tk.mainloop

View file

@ -1,320 +0,0 @@
#!/usr/bin/env ruby
#
$LOAD_PATH.unshift("lib")
$LOAD_PATH.unshift("test")
require 'madeleine'
require 'test/unit'
class Append
def initialize(value)
@value = value
end
def execute(system)
system << @value
end
end
module TestUtils
def delete_directory(directory_name)
return unless File.exists?(directory_name)
Dir.foreach(directory_name) do |file|
next if file == "."
next if file == ".."
assert(File.delete(directory_name + File::SEPARATOR + file) == 1,
"Unable to delete #{file}")
end
Dir.delete(directory_name)
end
end
class SnapshotMadeleineTest < Test::Unit::TestCase
include TestUtils
def teardown
delete_directory(persistence_base)
end
def persistence_base
"closing-test"
end
def test_closing
madeleine = SnapshotMadeleine.new(persistence_base) { "hello" }
madeleine.close
assert_raises(RuntimeError) do
madeleine.execute_command(Append.new("world"))
end
end
end
class NumberedFileTest < Test::Unit::TestCase
def test_main
target = Madeleine::NumberedFile.new(File::SEPARATOR + "foo", "bar", 321)
assert_equal(File::SEPARATOR + "foo" + File::SEPARATOR +
"000000000000000000321.bar",
target.name)
end
end
class LoggerTest < Test::Unit::TestCase
include TestUtils
def teardown
delete_directory("whoah")
end
def test_creation
@log = Object.new
def @log.store(command)
unless defined? @commands
@commands = []
end
@commands << command
end
def @log.commands
@commands
end
log_factory = self
target = Madeleine::Logger.new("whoah", log_factory)
target.store(:foo)
assert(@log.commands.include?(:foo))
end
# Self-shunt
def create_log(directory_name)
@log
end
end
class CommandVerificationTest < Test::Unit::TestCase
def teardown
Dir.delete("foo")
end
def test_broken_command
target = SnapshotMadeleine.new("foo") { :a_system }
assert_raises(Madeleine::InvalidCommandException) do
target.execute_command(:not_a_command)
end
end
end
class CustomMarshallerTest < Test::Unit::TestCase
include TestUtils
def teardown
delete_directory(prevalence_base)
end
def prevalence_base
"custom-marshaller-test"
end
def madeleine_class
SnapshotMadeleine
end
def test_changing_marshaller
@log = ""
marshaller = self
target = madeleine_class.new(prevalence_base, marshaller) { "hello world" }
target.take_snapshot
assert_equal("dump ", @log)
target = nil
madeleine_class.new(prevalence_base, marshaller) { flunk() }
assert_equal("dump load ", @log)
end
def load(io)
@log << "load "
assert_equal("dump data", io.read())
end
def dump(system, io)
@log << "dump "
assert_equal("hello world", system)
io.write("dump data")
end
end
class ErrorRaisingCommand
def execute(system)
raise "this is an exception from a command"
end
end
class ErrorHandlingTest < Test::Unit::TestCase
include TestUtils
def teardown
delete_directory(prevalence_base)
end
def prevalence_base
"error-handling-base"
end
def test_exception_in_command
madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" }
assert_raises(RuntimeError) do
madeleine.execute_command(ErrorRaisingCommand.new)
end
madeleine.close
madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" }
madeleine.close
end
end
class QueryTest < Test::Unit::TestCase
include TestUtils
def teardown
delete_directory(prevalence_base)
end
def prevalence_base
"query-base"
end
def test_querying
madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" }
query = Object.new
def query.execute(system)
system.size
end
# 'query' is an un-marshallable singleton, so we implicitly test
# that querys aren't stored.
assert_equal(5, madeleine.execute_query(query))
# TODO: assert that no logging was done
# TODO: assert that lock was held
end
end
class TimeOptimizingLoggerTest < Test::Unit::TestCase
include TestUtils
def setup
@target = Madeleine::Logger.new("some_directory", self)
@log = []
def @log.store(command)
self << command
end
end
def teardown
delete_directory("some_directory")
end
def test_optimizing_ticks
assert_equal(0, @log.size)
@target.store(Madeleine::Clock::Tick.new(Time.at(3)))
assert_equal(0, @log.size)
@target.store(Madeleine::Clock::Tick.new(Time.at(22)))
assert_equal(0, @log.size)
@target.store(Addition.new(100))
assert_kind_of(Madeleine::Clock::Tick, @log[0])
assert_equal(22, value_of_tick(@log[0]))
assert_equal(100, @log[1].value)
assert_equal(2, @log.size)
end
def value_of_tick(tick)
@clock = Object.new
def @clock.forward_to(time)
@value = time.to_i
end
def @clock.value
@value
end
tick.execute(self)
@clock.value
end
# Self-shunt
def create_log(directory_name)
assert_equal("some_directory", directory_name)
@log
end
# Self-shunt
def clock
@clock
end
end
class SharedLockQueryTest < Test::Unit::TestCase
include TestUtils
def prevalence_base
"shared_lock_test"
end
def teardown
delete_directory(prevalence_base)
end
def test_query
madeleine = SnapshotMadeleine.new(prevalence_base) { "hello" }
lock = Object.new
madeleine.instance_eval { @lock = lock } # FIXME: The horror, the horror
$shared = false
$was_shared = false
def lock.synchronize_shared(&block)
$shared = true
block.call
$shared = false
end
query = Object.new
def query.execute(system)
$was_shared = $shared
end
madeleine.execute_query(query)
assert($was_shared)
end
end
suite = Test::Unit::TestSuite.new("Madeleine")
suite << SnapshotMadeleineTest.suite
suite << NumberedFileTest.suite
require 'test_command_log'
suite << CommandLogTest.suite
suite << LoggerTest.suite
suite << CommandVerificationTest.suite
suite << CustomMarshallerTest.suite
suite << ErrorHandlingTest.suite
suite << QueryTest.suite
suite << TimeOptimizingLoggerTest.suite
suite << SharedLockQueryTest.suite
require 'test_executer'
suite << ExecuterTest.suite
require 'test_clocked'
add_clocked_tests(suite)
require 'test_automatic'
add_automatic_tests(suite)
require 'test_persistence'
add_persistence_tests(suite)
require 'test_platforms'
add_platforms_tests(suite)
require 'test_zmarshal'
add_zmarshal_tests(suite)
require 'test/unit/ui/console/testrunner'
Test::Unit::UI::Console::TestRunner.run(suite)

View file

@ -1,559 +0,0 @@
#!/usr/local/bin/ruby -w
#
# Copyright(c) 2003-2004 Stephen Sykes
# Copyright(c) 2003-2004 Anders Bengtsson
#
$LOAD_PATH.unshift("lib")
require 'madeleine'
require 'madeleine/automatic'
require 'test/unit'
#require 'contrib/batched.rb' # uncomment if testing batched
class A
include Madeleine::Automatic::Interceptor
attr_accessor :z,:k
def initialize
@k=1
end
end
class B
include Madeleine::Automatic::Interceptor
attr_accessor :yy, :s
def initialize(a)
@yy = C.new(a)
end
end
class C
include Madeleine::Automatic::Interceptor
attr_accessor :x, :a
def initialize(x)
@x = x
@a ||= D.new
end
end
# direct changes in this class are not saved, except at snapshot
class D
attr_accessor :w
end
class F
include Madeleine::Automatic::Interceptor
attr_accessor :z,:a
def plus1
@z += 1
end
end
class G
include Madeleine::Automatic::Interceptor
attr_accessor :yy,:a
def initialize
@yy = H.new
end
end
class H
include Madeleine::Automatic::Interceptor
attr_accessor :w
def minus1
@w -= 1
end
end
class I
include Madeleine::Automatic::Interceptor
def initialize
@x = J.new
end
def testyield
r = false
@x.yielder {|c| r = true if c == 1}
r
end
end
class J
include Madeleine::Automatic::Interceptor
def yielder
yield 1
end
end
class K
include Madeleine::Automatic::Interceptor
attr_accessor :k
def initialize
@k=1
end
def seven
@k=7
end
def fourteen
@k=14
end
automatic_read_only :fourteen
automatic_read_only
def twentyone
@k=21
end
end
class L
include Madeleine::Automatic::Interceptor
attr_reader :x
def initialize
@x = M.new(self)
end
end
class M
include Madeleine::Automatic::Interceptor
attr_reader :yy
def initialize(yy)
@yy = yy
end
end
class AutoTest < Test::Unit::TestCase
def persister
SnapshotMadeleine
end
def delete_directory(directory_name)
return unless File.exist?(directory_name)
Dir.foreach(directory_name) do |file|
next if file == "."
next if file == ".."
assert(File.delete(directory_name + File::SEPARATOR + file) == 1,
"Unable to delete #{file}")
end
Dir.delete(directory_name)
end
def create_new_system(klass, dir, *arg)
delete_directory(dir)
Thread.critical = true
@system_bases << dir
Thread.critical = false
make_system(dir) { klass.new(*arg) }
end
def make_system(dir, marshaller=Marshal, &block)
AutomaticSnapshotMadeleine.new(dir, marshaller, persister, &block)
end
def prevalence_base
"AutoPrevalenceTestBase" + self.class.to_s
end
def setup
@system_bases = []
end
def teardown
@system_bases.each {|dir|
delete_directory(dir)
}
end
def simpletest(n)
pb = prevalence_base + n.to_s
mad_a = create_new_system(A, pb)
mad_a.close
mad_a1 = make_system(pb) { A.new }
assert_equal(1, mad_a1.system.k, "No commands or snapshot")
mad_a1.system.z = 0
mad_a1.system.z += 1
assert_equal(1, mad_a1.system.z, "Object changes")
mad_a1.system.z -= 10
assert_equal(-9, mad_a1.system.z, "Object changes")
mad_a1.close
mad_a2 = make_system(pb) { A.new }
assert_equal(-9, mad_a2.system.z, "Commands but no snapshot")
mad_a2.take_snapshot
mad_a2.close
mad_a3 = make_system(pb) { A.new }
assert_equal(-9, mad_a3.system.z, "Snapshot but no commands")
mad_a3.system.z -= 6
mad_a3.system.z -= 3
mad_a3.close
mad_a4 = make_system(pb) { A.new }
assert_equal(-18, mad_a4.system.z, "Snapshot and commands")
mad_a4.close
end
end
# Basic test, and that system works in SAFE level 1
class BasicTest < AutoTest
def test_main
simpletest(1)
end
def test_main_in_safe_level_one
thread = Thread.new {
$SAFE = 1
test_main
}
thread.join
end
end
class ObjectOutsideTest < AutoTest
def test_main
mad = create_new_system(A, prevalence_base)
assert_raises(RuntimeError) {
mad.system.z = A.new # app object created outside system
}
mad.close
end
end
# Passing a block when it would generate a command is not allowed because blocks cannot
# be serialised. However, block passing/yielding inside the application is ok.
class BlockGivenTest < AutoTest
def test_main
mad = create_new_system(J, prevalence_base)
assert_raises(RuntimeError) {
mad.system.yielder {|a| a}
}
mad.close
mad2 = create_new_system(I, prevalence_base+"2")
assert(mad2.system.testyield, "Internal block passing")
mad2.close
end
end
class NonPersistedObjectTest < AutoTest
def test_main
mad_b = create_new_system(B, prevalence_base, 0)
mad_b.system.yy.x -= 1
assert_equal(-1, mad_b.system.yy.x, "Direct change of object inside main object")
mad_b.system.yy.a.w ||= "hello" # not saved
mad_b.system.yy.a.w += " again" # not saved
assert_equal("hello again", mad_b.system.yy.a.w, "Non persisted object before close")
mad_b.close
mad_b2 = make_system(prevalence_base) { B.new(0) }
assert_equal(nil, mad_b2.system.yy.a.w, "Non persisted object after restart, no snapshot")
mad_b2.system.yy.a.w ||= "hello" # not saved
mad_b2.system.yy.a.w += " again" # not saved
mad_b2.take_snapshot # NOW saved
mad_b2.system.yy.a.w += " again" # not saved
assert_equal("hello again again", mad_b2.system.yy.a.w, "Non persisted object after take_snapshot and 1 change")
mad_b2.close
mad_b3 = make_system(prevalence_base) { B.new(0) }
assert_equal("hello again", mad_b3.system.yy.a.w, "Non persisted object after restore (back to snapshotted state)")
mad_b3.close
end
end
class RefInExternalObjTest < AutoTest
def test_main
mad_c = create_new_system(B, prevalence_base, 0)
x = D.new
x.w = mad_c.system.yy
mad_c.system.s = x # pass in an external object that contains a ref to obj in ths system
mad_c.system.s.w.x += 1 # Increment counter via external obj
assert_equal(1, mad_c.system.yy.x, "Change via external object")
mad_c.system.yy.x += 1 # Increment counter directly
assert_equal(2, mad_c.system.s.w.x, "Direct change")
mad_c.close
mad_c2 = make_system(prevalence_base) { B.new(0) }
assert_equal(2, mad_c2.system.s.w.x, "Value via external object after commands/restore")
assert_equal(2, mad_c2.system.yy.x, "Direct value after restore")
mad_c2.take_snapshot
mad_c2.close
mad_c3 = make_system(prevalence_base) { B.new(0) }
assert_equal(2, mad_c3.system.s.w.x, "Value via external object after snapshot/restore")
assert_equal(2, mad_c3.system.yy.x, "Direct value after snapshot/restore")
mad_c3.system.s.w.x += 1 # Increment counter via external obj
mad_c3.system.yy.x += 1 # Increment counter directly
mad_c3.close
mad_c4 = make_system(prevalence_base) { B.new(0) }
assert_equal(4, mad_c4.system.s.w.x, "Value via external object after snapshot+commands/restore")
assert_equal(4, mad_c4.system.yy.x, "Direct value after snapshot+commands/restore")
mad_c4.close
end
end
class BasicThreadSafetyTest < AutoTest
def test_main
x = Thread.new {
simpletest(1)
}
y = Thread.new {
simpletest(2)
}
x.join
y.join
end
end
class ThreadConfidenceTest < AutoTest
def test_main
mad_d = create_new_system(F, prevalence_base)
mad_d.system.z = 0
mad_e = create_new_system(G, prevalence_base+"2")
mad_e.system.yy.w = 0
x = []
25.times {|n|
x[n] = Thread.new {
5.times {
sleep(rand/10)
mad_d.system.plus1
mad_e.system.yy.minus1
}
}
}
25.times {|n|
x[n].join
}
assert_equal(125, mad_d.system.z, "125 commands")
assert_equal(-125, mad_e.system.yy.w, "125 commands")
mad_e.close
mad_e2 = make_system(prevalence_base+"2") { G.new }
25.times {|n|
x[n] = Thread.new {
5.times {
sleep(rand/10)
mad_d.system.plus1
mad_e2.system.yy.minus1
}
}
}
25.times {|n|
x[n].join
}
assert_equal(250, mad_d.system.z, "restore/125 commands")
assert_equal(-250, mad_e2.system.yy.w, "restore/125 commands")
mad_d.close
mad_e2.close
end
end
class InvalidMethodTest < AutoTest
def test_main
mad_f = create_new_system(A, prevalence_base)
mad_f.system.z = -1
assert_raises(NoMethodError) {
mad_f.system.not_a_method
}
assert_equal(-1, mad_f.system.z, "System functions after NoMethodError")
mad_f.close
end
end
class CircularReferenceTest < AutoTest
def test_main
mad_g = create_new_system(G, prevalence_base)
mad_g.system.yy.w = mad_g.system
mad_g.close
mad_g2 = make_system(prevalence_base) { G.new }
assert(mad_g2.system == mad_g2.system.yy.w.yy.w.yy.w, "Circular reference after command/restore")
mad_g2.take_snapshot
mad_g2.close
mad_g3 = make_system(prevalence_base) { G.new }
assert(mad_g3.system == mad_g3.system.yy.w.yy.w.yy.w, "Circular reference after snapshot/restore")
mad_g3.system.yy.w.yy.w.yy.w.a = 1
assert_equal(1, mad_g3.system.a, "Circular reference change")
mad_g3.close
mad_g4 = make_system(prevalence_base) { G.new }
assert_equal(1, mad_g4.system.yy.w.yy.w.yy.w.a, "Circular reference after snapshot+commands/restore")
mad_g4.close
# The following tests would fail, cannot pass self (from class L to class M during init)
# self is the proxied object itself, not the Prox object it needs to be
mad_l = create_new_system(L, prevalence_base)
# assert_equal(mad_l.system, mad_l.system.x.yy, "Circular ref before snapshot/restore, passed self")
mad_l.take_snapshot
mad_l.close
mad_l = make_system(prevalence_base) { L.new }
# assert_equal(mad_l.system, mad_l.system.x.yy, "Circular ref after snapshot/restore, passed self")
mad_l.close
end
end
class AutomaticCustomMarshallerTest < AutoTest
def test_main
custom_m(YAML)
custom_m(SOAP::Marshal)
custom_m(Madeleine::ZMarshal.new)
custom_m(Madeleine::ZMarshal.new(YAML))
custom_m(Madeleine::ZMarshal.new(SOAP::Marshal))
end
def custom_m(marshaller)
dir = prevalence_base
delete_directory(dir)
@system_bases << dir
mad_h = make_system(dir) { G.new }
mad_h.system.yy.w = "abc"
mad_h.take_snapshot
mad_h.system.yy.w += "d"
assert_equal("abcd", mad_h.system.yy.w, "Custom marshalling after snapshot+commands with normal marshaller")
mad_h.close
mad_h = make_system(dir, marshaller) { G.new }
assert_equal("abcd", mad_h.system.yy.w, "Custom marshalling after snapshot+commands with normal marshaller, read with custom as marshaller")
mad_h.close
mad_h = make_system(dir) { G.new }
mad_h.marshaller = marshaller
mad_h.system.yy.w += "e"
assert_equal("abcde", mad_h.system.yy.w, "Custom marshalling after snapshot+commands+change marshaller+commands")
mad_h.take_snapshot
mad_h.close
if (marshaller == YAML)
File.open(dir + "/000000000000000000002.snapshot", "r") {|f|
assert_equal(f.gets, "--- !ruby/object:Madeleine::Automatic::Prox \n", "Custom marshalling marshaller change check")
}
end
mad_h = make_system(dir, marshaller) { G.new }
assert_equal("abcde", mad_h.system.yy.w,
"Custom marshalling after snapshot+commands+change marshaller+commands+snapshot+restore with normal marshaller")
mad_h.system.yy.w += "f"
mad_h.close
mad_h = make_system(dir) { G.new }
assert_equal("abcdef", mad_h.system.yy.w, "Custom marshalling snapshot custom+commands+restore normal")
mad_h.take_snapshot
mad_h.close
mad_h = make_system(dir, marshaller) { G.new }
assert_equal("abcdef", mad_h.system.yy.w, "Custom marshalling snapshot+restore custom")
mad_h.take_snapshot
mad_h.system.yy.w += "g"
mad_h.close
mad_h = make_system(dir, marshaller) { G.new }
assert_equal("abcdefg", mad_h.system.yy.w, "Custom marshalling after restore normal snapshot custom+commands+restore custom")
mad_h.system.yy.w = "abc"
mad_h.close
mad_h2 = make_system(dir, marshaller) { G.new }
assert_equal("abc", mad_h2.system.yy.w, "Custom marshalling after commands/restore")
mad_h2.take_snapshot
mad_h2.close
mad_h3 = make_system(dir, marshaller) { G.new }
assert_equal("abc", mad_h3.system.yy.w, "Custom marshalling after snapshot/restore")
mad_h3.system.yy.w += "d"
mad_h3.close
mad_h4 = make_system(dir, marshaller) { G.new }
assert_equal("abcd", mad_h4.system.yy.w, "Custom marshalling after snapshot+commands/restore")
mad_h4.close
mad_h = make_system(dir, marshaller) { G.new }
mad_h.system.yy.w = mad_h.system
mad_h.close
mad_h2 = make_system(dir, marshaller) { G.new }
assert_equal(mad_h2.system, mad_h2.system.yy.w, "Custom marshalling after commands/restore, circular ref")
mad_h2.take_snapshot
mad_h2.close
mad_h3 = make_system(dir, marshaller) { G.new }
assert_equal(mad_h3.system, mad_h3.system.yy.w, "Custom marshalling after snapshot/restore, circular ref")
mad_h3.system.yy.w = "sss"
mad_h3.system.yy.w = mad_h3.system
mad_h3.close
mad_h4 = make_system(dir, marshaller) { G.new }
assert_equal(mad_h4.system, mad_h4.system.yy.w, "Custom marshalling after snapshot+commands/restore, circular ref")
mad_h4.close
end
end
# tests thread safety during system creation, particularly that different system ids are generated
class ThreadedStartupTest < AutoTest
def test_main
x,mad = [],[]
20.times {|n|
x[n] = Thread.new {
sleep(rand/100)
mad[n] = create_new_system(F, prevalence_base+n.to_s)
mad[n].system.z = n
assert_equal(n, mad[n].system.z, "object change mad[#{n}].z")
mad[n].close
}
}
20.times {|n|
x[n].join
mad_i = make_system(prevalence_base+n.to_s) { F.new }
assert_equal(n, mad_i.system.z, "restored mad[#{n}].z")
mad_i.close
}
end
end
# tests restoring when objects get unreferenced and GC'd during restore
class FinalisedTest < AutoTest
def test_main
mad = create_new_system(B, prevalence_base, 0)
mad.system.yy = Array.new(200000) # make ruby run GC
mad.system.yy = Array.new(200000) # must be a better way, but running GC.start from inside
mad.system.yy = Array.new(50000) # class B didn't work for me
mad.close
mad2 = make_system(prevalence_base) { B.new(0) }
mad2.close
end
end
class DontInterceptTest < AutoTest
def test_main
mad = create_new_system(K, prevalence_base)
mad.system.seven
assert_equal(7, mad.system.k, "Object changes")
mad.system.fourteen
assert_equal(14, mad.system.k, "Object changes, not intercepted")
mad.system.twentyone
assert_equal(21, mad.system.k, "Object changes, not intercepted")
mad.close
mad_1 = make_system(prevalence_base) { K.new }
assert_equal(7, mad_1.system.k, "Commands but no snapshot")
mad_1.take_snapshot
mad_1.close
mad_2 = make_system(prevalence_base) { K.new }
assert_equal(7, mad_2.system.k, "Snapshot but no commands")
mad_2.system.k -= 6
mad_2.system.k -= 3
mad_2.system.fourteen
mad_2.close
mad_3 = make_system(prevalence_base) { K.new }
assert_equal(-2, mad_3.system.k, "Snapshot and commands")
mad_3.close
end
end
def add_automatic_tests(suite)
suite << BasicTest.suite
suite << ObjectOutsideTest.suite
suite << BlockGivenTest.suite
suite << NonPersistedObjectTest.suite
suite << RefInExternalObjTest.suite
suite << InvalidMethodTest.suite
suite << CircularReferenceTest.suite
suite << BasicThreadSafetyTest.suite
suite << FinalisedTest.suite
suite << DontInterceptTest.suite
suite << AutomaticCustomMarshallerTest.suite
end
def add_slow_automatic_tests(suite)
suite << ThreadConfidenceTest.suite
suite << ThreadedStartupTest.suite
end
if __FILE__ == $0
slowsuite = Test::Unit::TestSuite.new("AutomaticMadeleine (including slow tests)")
add_automatic_tests(slowsuite)
add_slow_automatic_tests(slowsuite)
require 'test/unit/ui/console/testrunner'
Test::Unit::UI::Console::TestRunner.run(slowsuite)
end

View file

@ -1,94 +0,0 @@
require 'madeleine/clock'
class ClockedAddingSystem
include Madeleine::Clock::ClockedSystem
attr_reader :total
def initialize
@total = 0
end
def add(value)
@total += value
@total
end
end
class TimeTest < Test::Unit::TestCase
def test_clock
target = Madeleine::Clock::Clock.new
assert_equal(0, target.time.to_i)
assert_equal(0, target.time.usec)
t1 = Time.at(10000)
target.forward_to(t1)
assert_equal(t1, target.time)
t2 = Time.at(20000)
target.forward_to(t2)
assert_equal(t2, target.time)
assert_nothing_raised() {
target.forward_to(t2)
}
end
def test_time_actor
@forward_calls = 0
@last_time = Time.at(0)
target = Madeleine::Clock::TimeActor.launch(self, 0.01)
# When launch() has returned it should have sent
# one synchronous clock-tick before it went to sleep
assert_equal(1, @forward_calls)
sleep(0.1)
assert(@forward_calls > 1)
target.destroy
@forward_calls = 0
sleep(0.1)
assert_equal(0, @forward_calls)
end
# Self-shunt
def execute_command(command)
mock_system = self
command.execute(mock_system)
end
# Self-shunt (system)
def clock
self
end
# Self-shunt (clock)
def forward_to(time)
if time < @last_time
raise "non-monotonous time"
end
@last_time = time
@forward_calls += 1
end
def test_clocked_system
target = Object.new
target.extend(Madeleine::Clock::ClockedSystem)
t1 = Time.at(10000)
target.clock.forward_to(t1)
assert_equal(t1, target.clock.time)
t2 = Time.at(20000)
target.clock.forward_to(t2)
assert_equal(t2, target.clock.time)
reloaded_target = Marshal.load(Marshal.dump(target))
assert_equal(t2, reloaded_target.clock.time)
end
end
def add_clocked_tests(suite)
suite << TimeTest.suite
end

View file

@ -1,110 +0,0 @@
unless $LOAD_PATH.include?("lib")
$LOAD_PATH.unshift("lib")
end
unless $LOAD_PATH.include?("test")
$LOAD_PATH.unshift("test")
end
require 'madeleine'
require 'test/unit'
require 'stringio'
class ExampleCommand
attr :value
def initialize(value)
@value = value
end
def execute(system)
system.add(@value)
end
end
class CommandLogTest < Test::Unit::TestCase
class MockFile < StringIO
def fsync
@was_fsynced = true
super
end
attr :was_fsynced
end
def test_file_opening
file_service = Object.new
def file_service.exist?(path)
[
["some", "path"].join(File::SEPARATOR),
["some", "path", "000000000000000000001.command_log"].join(File::SEPARATOR),
["some", "path", "000000000000000000002.command_log"].join(File::SEPARATOR),
["some", "path", "000000000000000000003.command_log"].join(File::SEPARATOR),
].include?(path)
end
def file_service.dir_entries(path, &block)
if path != ["some", "path"].join(File::SEPARATOR)
raise "wrong path"
end
[
"000000000000000000001.command_log",
"000000000000000000003.command_log",
"000000000000000000002.command_log",
]
end
def file_service.open(path, flags)
@was_open_called = true
if path != ["some", "path", "000000000000000000004.command_log"].join(File::SEPARATOR)
raise "wrong file id"
end
if flags != "wb"
raise "wrong flags"
end
MockFile.new
end
def file_service.was_open_called
@was_open_called
end
target = Madeleine::CommandLog.new("some/path", file_service)
assert(file_service.was_open_called)
end
def test_writing_command
file_service = Object.new
def file_service.exist?(path)
[
["some", "path"].join(File::SEPARATOR),
].include?(path)
end
def file_service.dir_entries(path, &block)
if path != ["some", "path"].join(File::SEPARATOR)
raise "wrong path"
end
[]
end
def file_service.open(path, flags)
@file = MockFile.new
@file
end
def file_service.file
@file
end
def file_service.verify
unless @file.was_fsynced
raise "file wasn't fsynced"
end
end
command = ExampleCommand.new(1234)
target = Madeleine::CommandLog.new("some/path", file_service)
target.store(command)
file_service.verify
file_service.file.rewind
assert_equal(Marshal.dump(command), file_service.file.read)
end
end

View file

@ -1,54 +0,0 @@
unless $LOAD_PATH.include?("lib")
$LOAD_PATH.unshift("lib")
end
unless $LOAD_PATH.include?("test")
$LOAD_PATH.unshift("test")
end
require 'test/unit'
require 'madeleine'
class ExecuterTest < Test::Unit::TestCase
def test_executer
system = Object.new
command = self
executer = Madeleine::Executer.new(system)
@executed_with = nil
executer.execute(command)
assert_same(system, @executed_with)
end
# Self-shunt
def execute(system)
@executed_with = system
end
def test_execute_with_exception
system = Object.new
command = Object.new
def command.execute(system)
raise "this is an exception from a command"
end
executer = Madeleine::Executer.new(system)
assert_raises(RuntimeError) {
executer.execute(command)
}
end
def test_exception_in_recovery
system = Object.new
command = Object.new
def command.execute(system)
raise "this is an exception from a command"
end
executer = Madeleine::Executer.new(system)
executer.recovery {
executer.execute(command)
}
assert_raises(RuntimeError) {
executer.execute(command)
}
end
end

View file

@ -1,169 +0,0 @@
#!/usr/local/bin/ruby -w
#
# Copyright(c) 2003 Anders Bengtsson
#
# PersistenceTest is based on the unit tests from Prevayler,
# Copyright(c) 2001-2003 Klaus Wuestefeld.
#
$LOAD_PATH.unshift("lib")
require 'madeleine'
require 'test/unit'
class AddingSystem
attr_reader :total
def initialize
@total = 0
end
def add(value)
@total += value
@total
end
end
class Addition
attr_reader :value
def initialize(value)
@value = value
end
def execute(system)
system.add(@value)
end
end
class PersistenceTest < Test::Unit::TestCase
def setup
@madeleine = nil
end
def teardown
delete_prevalence_files(prevalence_base)
Dir.delete(prevalence_base)
end
def verify(expected_total)
assert_equal(expected_total, prevalence_system().total(), "Total")
end
def prevalence_system
@madeleine.system
end
def prevalence_base
"PrevalenceBase"
end
def clear_prevalence_base
@madeleine.close unless @madeleine.nil?
delete_prevalence_files(prevalence_base())
end
def delete_prevalence_files(directory_name)
return unless File.exist?(directory_name)
Dir.foreach(directory_name) {|file_name|
next if file_name == '.'
next if file_name == '..'
file_name.untaint
assert(File.delete(directory_name + File::SEPARATOR + file_name) == 1,
"Unable to delete #{file_name}")
}
end
def crash_recover
@madeleine.close unless @madeleine.nil?
@madeleine = create_madeleine()
end
def create_madeleine
SnapshotMadeleine.new(prevalence_base()) { AddingSystem.new }
end
def snapshot
@madeleine.take_snapshot
end
def add(value, expected_total)
total = @madeleine.execute_command(Addition.new(value))
assert_equal(expected_total, total, "Total")
end
def verify_snapshots(expected_count)
count = 0
Dir.foreach(prevalence_base) {|name|
if name =~ /\.snapshot$/
count += 1
end
}
assert_equal(expected_count, count, "snapshots")
end
def test_main
clear_prevalence_base
# There is nothing to recover at first.
# A new system will be created.
crash_recover
crash_recover
add(40,40)
add(30,70)
verify(70)
crash_recover
verify(70)
add(20,90)
add(15,105)
verify_snapshots(0)
snapshot
verify_snapshots(1)
snapshot
verify_snapshots(2)
verify(105)
crash_recover
snapshot
add(10,115)
snapshot
add(5,120)
add(4,124)
verify(124)
crash_recover
add(3,127)
verify(127)
verify_snapshots(4)
clear_prevalence_base
snapshot
crash_recover
add(10,137)
add(2,139)
crash_recover
verify(139)
end
def test_main_in_safe_level_one
thread = Thread.new {
$SAFE = 1
test_main
}
thread.join
end
end
def add_persistence_tests(suite)
suite << PersistenceTest.suite
end

View file

@ -1,65 +0,0 @@
class AddCommand
def initialize(obj)
@obj = obj
end
def execute(system)
system[@obj.myid] = @obj
end
end
class Foo
attr_accessor :myid
end
# Checks for a strange marshalling or IO bug observed in the
# native win32-port of Ruby on WinXP.
#
# Test case provided by Steve Conover.
class WierdWin32CorruptionTest < Test::Unit::TestCase
include TestUtils
def teardown
(1..5).each {|i|
delete_directory("corruption_test#{i}")
}
end
def doCorruptionTest(idstr, storagenumber)
m = SnapshotMadeleine.new("corruption_test" + storagenumber) { Hash.new() }
f = Foo.new()
f.myid = idstr
m.execute_command(AddCommand.new(f))
m.close()
m = SnapshotMadeleine.new("corruption_test" + storagenumber) { Hash.new() }
end
def testErrorOne
doCorruptionTest("123456789012345678901", "1")
end
def testErrorTwo
doCorruptionTest("aaaaaaaaaaaaaaaaaaaaa", "2")
end
def testNoErrorOne
doCorruptionTest("12345678901234567890", "3")
end
def testNoErrorTwo
doCorruptionTest("1234567890123456789012", "4")
end
def testWhiteSpace
doCorruptionTest("\n\r\t \r\n", "5")
end
end
def add_platforms_tests(suite)
suite << WierdWin32CorruptionTest.suite
end

View file

@ -1,52 +0,0 @@
require 'madeleine/zmarshal'
require 'stringio'
require 'yaml'
class ZMarshalTest < Test::Unit::TestCase
def test_full_circle_marshal
target = Madeleine::ZMarshal.new(Marshal)
object = ["foo", "bar"]
stream = StringIO.new
target.dump(object, stream)
stream.rewind
result = target.load(stream)
assert_equal(object, result)
end
def test_full_circle_yaml
target = Madeleine::ZMarshal.new(YAML)
object = ["foo", "bar"]
stream = StringIO.new
target.dump(object, stream)
stream.rewind
result = target.load(stream)
assert_equal(object, result)
end
def test_compression
target = Madeleine::ZMarshal.new(Marshal)
object = "x" * 1000
stream = StringIO.new
Marshal.dump(object, stream)
reference_size = stream.size
stream = StringIO.new
target.dump(object, stream)
compressed_size = stream.size
assert(compressed_size < reference_size)
end
end
def add_zmarshal_tests(suite)
suite << ZMarshalTest.suite
end