madeleine 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +31 -0
- data/NEWS +55 -0
- data/README +78 -0
- data/contrib/batched.rb +298 -0
- data/contrib/benchmark.rb +35 -0
- data/contrib/test_batched.rb +245 -0
- data/contrib/test_scalability.rb +248 -0
- data/contrib/threaded_benchmark.rb +44 -0
- data/lib/madeleine.rb +419 -0
- data/lib/madeleine/automatic.rb +418 -0
- data/lib/madeleine/clock.rb +94 -0
- data/lib/madeleine/files.rb +19 -0
- data/lib/madeleine/zmarshal.rb +60 -0
- data/samples/clock_click.rb +73 -0
- data/samples/dictionary_client.rb +23 -0
- data/samples/dictionary_server.rb +94 -0
- data/samples/painter.rb +60 -0
- metadata +53 -0
@@ -0,0 +1,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
|