madeleine 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +31 -0
- data/NEWS +55 -0
- data/README +78 -0
- data/contrib/batched.rb +298 -0
- data/contrib/benchmark.rb +35 -0
- data/contrib/test_batched.rb +245 -0
- data/contrib/test_scalability.rb +248 -0
- data/contrib/threaded_benchmark.rb +44 -0
- data/lib/madeleine.rb +419 -0
- data/lib/madeleine/automatic.rb +418 -0
- data/lib/madeleine/clock.rb +94 -0
- data/lib/madeleine/files.rb +19 -0
- data/lib/madeleine/zmarshal.rb +60 -0
- data/samples/clock_click.rb +73 -0
- data/samples/dictionary_client.rb +23 -0
- data/samples/dictionary_server.rb +94 -0
- data/samples/painter.rb +60 -0
- metadata +53 -0
@@ -0,0 +1,35 @@
|
|
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 = BatchedSnapshotMadeleine.new("benchmark-base") { :the_system }
|
19
|
+
|
20
|
+
RUNS = 2000
|
21
|
+
|
22
|
+
GC.start
|
23
|
+
GC.disable
|
24
|
+
|
25
|
+
t0 = Time.now
|
26
|
+
RUNS.times {
|
27
|
+
madeleine.execute_command(BenchmarkCommand.new(1234))
|
28
|
+
}
|
29
|
+
t1 = Time.now
|
30
|
+
|
31
|
+
GC.enable
|
32
|
+
|
33
|
+
tps = RUNS/(t1 - t0)
|
34
|
+
|
35
|
+
puts "#{tps.to_i} transactions/s"
|
@@ -0,0 +1,245 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
#
|
3
|
+
# Copyright(c) 2003 H�kan R�berg
|
4
|
+
#
|
5
|
+
# Some components taken from test_persistence.rb
|
6
|
+
# Copyright(c) 2003 Anders Bengtsson
|
7
|
+
#
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift("../lib")
|
10
|
+
|
11
|
+
require 'batched'
|
12
|
+
require 'test/unit'
|
13
|
+
require 'madeleine/clock'
|
14
|
+
|
15
|
+
|
16
|
+
module Madeleine::Batch
|
17
|
+
class BatchedSnapshotMadeleineTest < Test::Unit::TestCase
|
18
|
+
|
19
|
+
class ArraySystem < Array
|
20
|
+
include Madeleine::Clock::ClockedSystem
|
21
|
+
end
|
22
|
+
|
23
|
+
class PushCommand
|
24
|
+
def initialize(value)
|
25
|
+
@value = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute(system)
|
29
|
+
system << @value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ArrayQuery
|
34
|
+
def initialize
|
35
|
+
@time = []
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute(system)
|
39
|
+
length = system.length
|
40
|
+
@time << system.clock.time
|
41
|
+
|
42
|
+
a = 1
|
43
|
+
system.each do |n|
|
44
|
+
a *= n
|
45
|
+
end
|
46
|
+
|
47
|
+
raise "inconsistent read" unless length == system.length
|
48
|
+
raise "inconsistent read" unless @time.last == system.clock.time
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_live_snapshot
|
53
|
+
system = ArraySystem.new
|
54
|
+
w, r = [], []
|
55
|
+
going = true
|
56
|
+
|
57
|
+
madeleine = BatchedSnapshotMadeleine.new(prevalence_base) { system }
|
58
|
+
|
59
|
+
i = 0
|
60
|
+
10.times do |n|
|
61
|
+
w[n] = Thread.new {
|
62
|
+
while going
|
63
|
+
madeleine.execute_command(PushCommand.new(i))
|
64
|
+
i += 1
|
65
|
+
sleep(0.1)
|
66
|
+
end
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
q = 0
|
71
|
+
query = ArrayQuery.new
|
72
|
+
100.times do |n|
|
73
|
+
r[n] = Thread.new {
|
74
|
+
while going
|
75
|
+
begin
|
76
|
+
madeleine.execute_query(query)
|
77
|
+
q += 1
|
78
|
+
rescue
|
79
|
+
fail("Query blocks writing")
|
80
|
+
end
|
81
|
+
sleep(0.1)
|
82
|
+
end
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
s = 0
|
87
|
+
snapshot = Thread.new {
|
88
|
+
while going
|
89
|
+
madeleine.take_snapshot
|
90
|
+
s += 1
|
91
|
+
sleep(0.01)
|
92
|
+
end
|
93
|
+
}
|
94
|
+
|
95
|
+
sleep(1)
|
96
|
+
|
97
|
+
going = false
|
98
|
+
|
99
|
+
r.each do |t|
|
100
|
+
t.join
|
101
|
+
end
|
102
|
+
|
103
|
+
w.each do |t|
|
104
|
+
t.join
|
105
|
+
end
|
106
|
+
|
107
|
+
snapshot.join
|
108
|
+
|
109
|
+
madeleine.close
|
110
|
+
|
111
|
+
madeleine2 = SnapshotMadeleine.new(prevalence_base)
|
112
|
+
assert_equal(madeleine.system, madeleine2.system, "Take system snapshots while accessing")
|
113
|
+
end
|
114
|
+
|
115
|
+
def prevalence_base
|
116
|
+
"BatchedSnapshot"
|
117
|
+
end
|
118
|
+
|
119
|
+
def teardown
|
120
|
+
delete_directory(prevalence_base)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class BatchedLogTest < Test::Unit::TestCase
|
125
|
+
|
126
|
+
class MockMadeleine
|
127
|
+
def initialize(logger)
|
128
|
+
@logger = logger
|
129
|
+
end
|
130
|
+
|
131
|
+
def flush
|
132
|
+
@logger.flush
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class MockCommand
|
137
|
+
attr_reader :text
|
138
|
+
|
139
|
+
def initialize(text)
|
140
|
+
@text = text
|
141
|
+
end
|
142
|
+
|
143
|
+
def execute(system)
|
144
|
+
end
|
145
|
+
|
146
|
+
def ==(o)
|
147
|
+
o.text == @text
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module BufferInspector
|
152
|
+
def buffer_size
|
153
|
+
@buffer.size
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def setup
|
158
|
+
@target = BatchedLogger.new(".", BatchedLogFactory.new, nil)
|
159
|
+
@target.extend(BufferInspector)
|
160
|
+
@madeleine = MockMadeleine.new(@target)
|
161
|
+
@messages = []
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_logging
|
165
|
+
actor = LogActor.launch(@madeleine, 0.1)
|
166
|
+
|
167
|
+
append("Hello")
|
168
|
+
sleep(0.01)
|
169
|
+
append("World")
|
170
|
+
sleep(0.01)
|
171
|
+
|
172
|
+
assert_equal(2, @target.buffer_size, "Batched command queue")
|
173
|
+
assert(!File.exist?(expected_file_name), "Batched commands not on disk")
|
174
|
+
|
175
|
+
sleep(0.2)
|
176
|
+
|
177
|
+
assert_equal(0, @target.buffer_size, "Queue emptied by batched write")
|
178
|
+
file_size = File.size(expected_file_name)
|
179
|
+
assert(file_size > 0, "Queue written to disk")
|
180
|
+
|
181
|
+
append("Again")
|
182
|
+
sleep(0.2)
|
183
|
+
|
184
|
+
assert(File.size(expected_file_name) > file_size, "Command written to disk")
|
185
|
+
|
186
|
+
f = File.new(expected_file_name)
|
187
|
+
|
188
|
+
@messages.each do |message|
|
189
|
+
assert_equal(message, Marshal.load(f), "Commands logged in order")
|
190
|
+
end
|
191
|
+
|
192
|
+
f.close
|
193
|
+
|
194
|
+
actor.destroy
|
195
|
+
@target.flush
|
196
|
+
@target.close
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
def append(text)
|
201
|
+
Thread.new {
|
202
|
+
message = MockCommand.new(text)
|
203
|
+
@messages << message
|
204
|
+
queued_command = QueuedCommand.new(message)
|
205
|
+
@target.store(queued_command)
|
206
|
+
queued_command.wait_for
|
207
|
+
}
|
208
|
+
end
|
209
|
+
|
210
|
+
def expected_file_name
|
211
|
+
"000000000000000000001.command_log"
|
212
|
+
end
|
213
|
+
|
214
|
+
def teardown
|
215
|
+
assert(File.delete(expected_file_name) == 1)
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
def delete_directory(directory_name)
|
221
|
+
Dir.foreach(directory_name) do |file|
|
222
|
+
next if file == "."
|
223
|
+
next if file == ".."
|
224
|
+
assert(File.delete(directory_name + File::SEPARATOR + file) == 1,
|
225
|
+
"Unable to delete #{file}")
|
226
|
+
end
|
227
|
+
Dir.delete(directory_name)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
include Madeleine::Batch
|
232
|
+
|
233
|
+
def add_batched_tests(suite)
|
234
|
+
suite << BatchedSnapshotMadeleineTest.suite
|
235
|
+
suite << BatchedLogTest.suite
|
236
|
+
end
|
237
|
+
|
238
|
+
if __FILE__ == $0
|
239
|
+
suite = Test::Unit::TestSuite.new("BatchedLogTest")
|
240
|
+
add_batched_tests(suite)
|
241
|
+
|
242
|
+
require 'test/unit/ui/console/testrunner'
|
243
|
+
Thread.abort_on_exception = true
|
244
|
+
Test::Unit::UI::Console::TestRunner.run(suite)
|
245
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
#
|
3
|
+
# Copyright(c) 2003 H�kan R�berg
|
4
|
+
#
|
5
|
+
# This test is based on Prevaylers TransactionTestRun,
|
6
|
+
# Copyright(c) 2001-2003 Klaus Wuestefeld.
|
7
|
+
#
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift("../lib")
|
10
|
+
|
11
|
+
require 'madeleine'
|
12
|
+
require 'madeleine/clock'
|
13
|
+
require 'batched'
|
14
|
+
|
15
|
+
module ScalabilityTest
|
16
|
+
|
17
|
+
class TransactionTestRun
|
18
|
+
MIN_THREADS = 20
|
19
|
+
MAX_THREADS = 20
|
20
|
+
NUMBER_OF_OBJECTS = 100000
|
21
|
+
ROUND_DURATION = 20
|
22
|
+
DIR = "ScalabilityBase"
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@system = TransactionSystem.new
|
26
|
+
@madeleine = BatchedSnapshotMadeleine.new(DIR) { @system }
|
27
|
+
|
28
|
+
@system.replace_all_records(create_records(NUMBER_OF_OBJECTS))
|
29
|
+
|
30
|
+
@is_round_finished = false
|
31
|
+
|
32
|
+
@best_round_ops_per_s = 0
|
33
|
+
@best_round_threads = 0
|
34
|
+
@operation_count = 0
|
35
|
+
@last_operation = 0
|
36
|
+
@active_round_threads = 0
|
37
|
+
|
38
|
+
@half_of_the_objects = NUMBER_OF_OBJECTS / 2
|
39
|
+
|
40
|
+
@connection_cache = []
|
41
|
+
@connection_cache_lock = Mutex.new
|
42
|
+
|
43
|
+
ObjectSpace.garbage_collect
|
44
|
+
|
45
|
+
puts "========= Running " + name + " (" + (MAX_THREADS - MIN_THREADS + 1).to_s + " rounds). Subject: " + subject_name + "..."
|
46
|
+
puts "Each round will take approx. " + ROUND_DURATION.to_s + " seconds to run..."
|
47
|
+
perform_test
|
48
|
+
puts "----------- BEST ROUND: " + result_string(@best_round_ops_per_s, @best_round_threads)
|
49
|
+
|
50
|
+
@madeleine.close
|
51
|
+
delete_directory(DIR)
|
52
|
+
end
|
53
|
+
|
54
|
+
def name
|
55
|
+
"Transaction Test"
|
56
|
+
end
|
57
|
+
|
58
|
+
def subject_name
|
59
|
+
"Madeleine"
|
60
|
+
end
|
61
|
+
|
62
|
+
def result_string(ops_per_s, threads)
|
63
|
+
ops_per_s.to_s + " operations/second (" + threads.to_s + " threads)"
|
64
|
+
end
|
65
|
+
|
66
|
+
def perform_test
|
67
|
+
for threads in MIN_THREADS..MAX_THREADS
|
68
|
+
ops_per_s = perform_round(threads)
|
69
|
+
if ops_per_s > @best_round_ops_per_s
|
70
|
+
@best_round_ops_per_s = ops_per_s
|
71
|
+
@best_round_threads = threads
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def perform_round(threads)
|
77
|
+
initial_operation_count = @operation_count
|
78
|
+
start_time = Time.now.to_f
|
79
|
+
|
80
|
+
start_threads(threads)
|
81
|
+
sleep(ROUND_DURATION)
|
82
|
+
stop_threads
|
83
|
+
|
84
|
+
seconds_ellapsed = Time.now.to_f - start_time
|
85
|
+
ops_per_second = (@operation_count - initial_operation_count) / seconds_ellapsed
|
86
|
+
|
87
|
+
puts
|
88
|
+
puts "Seconds ellapsed: " + seconds_ellapsed.to_s
|
89
|
+
puts "--------- Round Result: " + result_string(ops_per_second, threads)
|
90
|
+
|
91
|
+
ops_per_second
|
92
|
+
end
|
93
|
+
|
94
|
+
def start_threads(threads)
|
95
|
+
@is_round_finished = false
|
96
|
+
for i in 1..threads
|
97
|
+
start_thread(@last_operation + i, threads)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def start_thread(starting_operation, operation_increment)
|
102
|
+
Thread.new {
|
103
|
+
connection = accquire_connection
|
104
|
+
|
105
|
+
operation = starting_operation
|
106
|
+
while not @is_round_finished
|
107
|
+
# puts "Operation " + operation.to_s
|
108
|
+
execute_operation(connection, operation)
|
109
|
+
operation += operation_increment
|
110
|
+
end
|
111
|
+
|
112
|
+
@connection_cache_lock.synchronize do
|
113
|
+
@connection_cache << connection
|
114
|
+
@operation_count += (operation - starting_operation) / operation_increment
|
115
|
+
@last_operation = operation if @last_operation < operation
|
116
|
+
@active_round_threads -= 1
|
117
|
+
end
|
118
|
+
}
|
119
|
+
@active_round_threads += 1
|
120
|
+
end
|
121
|
+
|
122
|
+
def execute_operation(connection, operation)
|
123
|
+
record_to_insert = Record.new(NUMBER_OF_OBJECTS + operation)
|
124
|
+
id_to_delete = spread_id(operation)
|
125
|
+
record_to_update = Record.new(@half_of_the_objects + id_to_delete)
|
126
|
+
|
127
|
+
connection.perform_transaction(record_to_insert, record_to_update, id_to_delete)
|
128
|
+
end
|
129
|
+
|
130
|
+
def spread_id(id)
|
131
|
+
(id / @half_of_the_objects) * @half_of_the_objects + ((id * 16807) % @half_of_the_objects)
|
132
|
+
end
|
133
|
+
|
134
|
+
def create_test_connection
|
135
|
+
TransactionConnection.new(@madeleine)
|
136
|
+
end
|
137
|
+
|
138
|
+
def accquire_connection
|
139
|
+
@connection_cache_lock.synchronize do
|
140
|
+
return @connection_cache.empty? ? create_test_connection : @connection_cache.shift
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def stop_threads
|
145
|
+
@is_round_finished = true
|
146
|
+
while @active_round_threads != 0
|
147
|
+
sleep(0.001)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def create_records(number_of_objects)
|
152
|
+
result = []
|
153
|
+
for i in 0..number_of_objects
|
154
|
+
result << Record.new(i)
|
155
|
+
end
|
156
|
+
result
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def delete_directory(directory_name)
|
161
|
+
Dir.foreach(directory_name) do |file|
|
162
|
+
next if file == "."
|
163
|
+
next if file == ".."
|
164
|
+
File.delete(directory_name + File::SEPARATOR + file)
|
165
|
+
end
|
166
|
+
Dir.delete(directory_name)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class TransactionSystem
|
171
|
+
include Madeleine::Clock::ClockedSystem
|
172
|
+
|
173
|
+
def initialize
|
174
|
+
@records_by_id = Hash.new
|
175
|
+
@transaction_lock = Mutex.new
|
176
|
+
end
|
177
|
+
|
178
|
+
def perform_transaction(record_to_insert, record_to_update, id_to_delete)
|
179
|
+
@transaction_lock.synchronize do
|
180
|
+
put(record_to_insert)
|
181
|
+
put(record_to_update)
|
182
|
+
@records_by_id.delete(id_to_delete)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def put(new_record)
|
187
|
+
@records_by_id[new_record.id] = new_record
|
188
|
+
end
|
189
|
+
|
190
|
+
def replace_all_records(new_records)
|
191
|
+
@records_by_id.clear
|
192
|
+
new_records.each do |record|
|
193
|
+
put(record)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class TransactionConnection
|
199
|
+
def initialize(madeleine)
|
200
|
+
@madeleine = madeleine
|
201
|
+
end
|
202
|
+
|
203
|
+
def perform_transaction(record_to_insert, record_to_update, id_to_delete)
|
204
|
+
@madeleine.execute_command(TestTransaction.new(record_to_insert, record_to_update, id_to_delete))
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class TestTransaction
|
209
|
+
def initialize(record_to_insert, record_to_update, id_to_delete)
|
210
|
+
@record_to_insert = record_to_insert
|
211
|
+
@record_to_update = record_to_update
|
212
|
+
@id_to_delete = id_to_delete
|
213
|
+
end
|
214
|
+
|
215
|
+
def execute(system)
|
216
|
+
system.perform_transaction(@record_to_insert, @record_to_update, @id_to_delete)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class Record
|
221
|
+
attr_reader :id, :name, :string_1, :date_1, :date_2
|
222
|
+
|
223
|
+
def initialize(id)
|
224
|
+
@id = id
|
225
|
+
@name = "NAME" + (id % 10000).to_s
|
226
|
+
@string_1 = (id % 10000).to_s == 0 ? Record.large_string + id : nil;
|
227
|
+
@date_1 = Record.random_date
|
228
|
+
@date_2 = Record.random_date
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.large_string
|
232
|
+
[].fill("A", 1..980).to_s
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.random_date
|
236
|
+
rand(10000000)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
if __FILE__ == $0
|
242
|
+
puts "Madeleine Scalability Test"
|
243
|
+
puts "Based on Prevaylers Scalability Test"
|
244
|
+
puts
|
245
|
+
|
246
|
+
Thread.abort_on_exception = true
|
247
|
+
ScalabilityTest::TransactionTestRun.new
|
248
|
+
end
|