rubyrep 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,6 +8,9 @@ module RR
8
8
  # The current Session object
9
9
  attr_accessor :session
10
10
 
11
+ # The current TaskSweeper
12
+ attr_accessor :sweeper
13
+
11
14
  # Returns the current ReplicationHelper; creates it if necessary
12
15
  def helper
13
16
  @helper ||= ReplicationHelper.new(self)
@@ -22,22 +25,29 @@ module RR
22
25
  # Executes the replication run.
23
26
  def run
24
27
  return unless [:left, :right].any? do |database|
25
- Timeout::timeout(session.configuration.options[:database_connection_timeout]) do
26
- session.send(database).select_one(
28
+ changes_pending = false
29
+ t = Thread.new do
30
+ changes_pending = session.send(database).select_one(
27
31
  "select id from #{session.configuration.options[:rep_prefix]}_pending_changes"
28
32
  ) != nil
29
33
  end
34
+ t.join session.configuration.options[:database_connection_timeout]
35
+ changes_pending
30
36
  end
37
+
38
+ loaders = LoggedChangeLoaders.new(session)
39
+
31
40
  begin
32
41
  success = false
33
42
  replicator # ensure that replicator is created and has chance to validate settings
34
43
 
35
44
  loop do
36
45
  begin
37
- session.reload_changes # ensure the cache of change log records is up-to-date
38
- diff = ReplicationDifference.new session
46
+ loaders.update # ensure the cache of change log records is up-to-date
47
+ diff = ReplicationDifference.new loaders
39
48
  diff.load
40
49
  break unless diff.loaded?
50
+ raise "Replication run timed out" if sweeper.terminated?
41
51
  replicator.replicate_difference diff if diff.type != :no_diff
42
52
  rescue Exception => e
43
53
  helper.log_replication_outcome diff, e.message,
@@ -50,10 +60,23 @@ module RR
50
60
  end
51
61
  end
52
62
 
63
+ # Installs the current sweeper into the database connections
64
+ def install_sweeper
65
+ [:left, :right].each do |database|
66
+ unless session.send(database).respond_to?(:sweeper)
67
+ session.send(database).send(:extend, NoisyConnection)
68
+ end
69
+ session.send(database).sweeper = sweeper
70
+ end
71
+ end
72
+
53
73
  # Creates a new ReplicationRun instance.
54
74
  # * +session+: the current Session
55
- def initialize(session)
75
+ # * +sweeper+: the current TaskSweeper
76
+ def initialize(session, sweeper)
56
77
  self.session = session
78
+ self.sweeper = sweeper
79
+ install_sweeper
57
80
  end
58
81
  end
59
- end
82
+ end
@@ -108,8 +108,12 @@ EOS
108
108
  until termination_requested do
109
109
  begin
110
110
  session.refresh
111
- run = ReplicationRun.new session
112
- run.run
111
+ timeout = session.configuration.options[:database_connection_timeout]
112
+ terminated = TaskSweeper.timeout(timeout) do |sweeper|
113
+ run = ReplicationRun.new session, sweeper
114
+ run.run
115
+ end.terminated?
116
+ raise "replication run timed out" if terminated
113
117
  rescue Exception => e
114
118
  now = Time.now.iso8601
115
119
  $stderr.puts "#{now} Exception caught: #{e}"
@@ -83,32 +83,7 @@ module RR
83
83
  @table_map[db_arm][table] || table
84
84
  end
85
85
 
86
- # Does the actual work of establishing a database connection
87
- # db_arm:: should be either :left or :right
88
- # config:: the rubyrep Configuration
89
- def db_connect(db_arm, config)
90
- arm_config = config.send db_arm
91
- @proxies[db_arm] = DatabaseProxy.new
92
- @connections[db_arm] = @proxies[db_arm].create_session arm_config
93
- end
94
-
95
- # Does the actual work of establishing a proxy connection
96
- # db_arm:: should be either :left or :right
97
- # config:: the rubyrep Configuration
98
- def proxy_connect(db_arm, config)
99
- arm_config = config.send db_arm
100
- if arm_config.include? :proxy_host
101
- drb_url = "druby://#{arm_config[:proxy_host]}:#{arm_config[:proxy_port]}"
102
- @proxies[db_arm] = DRbObject.new nil, drb_url
103
- else
104
- # If one connection goes through a proxy, so has the other one.
105
- # So if necessary, create a "fake" proxy
106
- @proxies[db_arm] = DatabaseProxy.new
107
- end
108
- @connections[db_arm] = @proxies[db_arm].create_session arm_config
109
- end
110
-
111
- # True if proxy connections are used
86
+ # Returns +true+ if proxy connections are used
112
87
  def proxied?
113
88
  [configuration.left, configuration.right].any? \
114
89
  {|arm_config| arm_config.include? :proxy_host}
@@ -144,9 +119,90 @@ module RR
144
119
  end
145
120
  end
146
121
 
147
- # Refreshes (reestablish if no more active) the database connections.
122
+ # Returns +true+ if the specified database connection is not alive.
123
+ # * +database+: target database (either +:left+ or :+right+)
124
+ def database_unreachable?(database)
125
+ unreachable = true
126
+ Thread.new do
127
+ begin
128
+ if send(database) && send(database).select_one("select 1+1 as x")['x'].to_i == 2
129
+ unreachable = false # database is actually reachable
130
+ end
131
+ end rescue nil
132
+ end.join configuration.options[:database_connection_timeout]
133
+ unreachable
134
+ end
135
+
136
+ # Disconnnects the specified database
137
+ # * +database+: the target database (either :+left+ or :+right+)
138
+ def disconnect_database(database)
139
+ proxy, connection = @proxies[database], @connection[database]
140
+ @proxies[database] = nil
141
+ @connections[database] = nil
142
+ if proxy
143
+ proxy.destroy_session(connection)
144
+ end
145
+ end
146
+
147
+ # Refreshes both database connections
148
148
  def refresh
149
- [:left, :right].each {|database| send(database).refresh}
149
+ [:left, :right].each {|database| refresh_database_connection database}
150
+ end
151
+
152
+ # Refreshes the specified database connection.
153
+ # (I. e. reestablish if not active anymore.)
154
+ # * +database+: target database (either :+left+ or :+right+)
155
+ def refresh_database_connection(database)
156
+ if database_unreachable?(database)
157
+ # step 1: disconnect both database connection (if still possible)
158
+ begin
159
+ Thread.new do
160
+ disconnect_database database rescue nil
161
+ end.join configuration.options[:database_connection_timeout]
162
+ end
163
+
164
+ connect_exception = nil
165
+ # step 2: try to reconnect the database
166
+ Thread.new do
167
+ begin
168
+ connect_database database
169
+ rescue Exception => e
170
+ # save exception so it can be rethrown outside of the thread
171
+ connect_exception = e
172
+ end
173
+ end.join configuration.options[:database_connection_timeout]
174
+ raise connect_exception if connect_exception
175
+
176
+ # step 3: verify if database connections actually work (to detect silent connection failures)
177
+ if database_unreachable?(database)
178
+ raise "no connection to '#{database}' database"
179
+ end
180
+ end
181
+ end
182
+
183
+ # Set up the (proxied or direct) database connections to the specified
184
+ # database.
185
+ # * +database+: the target database (either :+left+ or :+right+)
186
+ def connect_database(database)
187
+ if configuration.left == configuration.right and database == :right
188
+ # If both database configurations point to the same database
189
+ # then don't create the database connection twice.
190
+ # Assumes that the left database is always connected before the right one.
191
+ self.right = self.left
192
+ else
193
+ # Connect the database / proxy
194
+ arm_config = configuration.send database
195
+ if arm_config.include? :proxy_host
196
+ drb_url = "druby://#{arm_config[:proxy_host]}:#{arm_config[:proxy_port]}"
197
+ @proxies[database] = DRbObject.new nil, drb_url
198
+ else
199
+ # Create fake proxy
200
+ @proxies[database] = DatabaseProxy.new
201
+ end
202
+ @connections[database] = @proxies[database].create_session arm_config
203
+
204
+ send(database).manual_primary_keys = manual_primary_keys(database)
205
+ end
150
206
  end
151
207
 
152
208
  # Creates a new rubyrep session with the provided Configuration
@@ -157,21 +213,7 @@ module RR
157
213
  # Keep the database configuration for future reference
158
214
  self.configuration = config
159
215
 
160
- # Determine method of connection (either 'proxy_connect' or 'db_connect'
161
- connection_method = proxied? ? :proxy_connect : :db_connect
162
-
163
- # Connect the left database / proxy
164
- self.send connection_method, :left, configuration
165
- left.manual_primary_keys = manual_primary_keys(:left)
166
-
167
- # If both database configurations point to the same database
168
- # then don't create the database connection twice
169
- if configuration.left == configuration.right
170
- self.right = self.left
171
- else
172
- self.send connection_method, :right, configuration
173
- right.manual_primary_keys = manual_primary_keys(:right)
174
- end
216
+ refresh
175
217
  end
176
218
  end
177
219
  end
@@ -0,0 +1,77 @@
1
+ module RR
2
+
3
+ # Monitors and cancels stalled tasks
4
+ class TaskSweeper
5
+
6
+ # Executes the give block in a separate Thread.
7
+ # Returns if block is finished or stalled.
8
+ # The block must call regular #ping to announce it is not stalled.
9
+ # * +timeout_period+:
10
+ # Maximum time (in seonds) without ping, after which a task is considered stalled.
11
+ # Returns the created sweeper (allows checking if task was terminated).
12
+ def self.timeout(timeout_period)
13
+ sweeper = TaskSweeper.new(timeout_period)
14
+ sweeper.send(:timeout) {yield sweeper}
15
+ sweeper
16
+ end
17
+
18
+ # Time in seconds after which a task is considered stalled. Timer is reset
19
+ # by calling #ping.
20
+ attr_accessor :timeout_period
21
+
22
+ # Must be called by the executed task to announce it is still alive
23
+ def ping
24
+ self.last_ping = Time.now
25
+ end
26
+
27
+ # Returns +true+ if the task was timed out.
28
+ # The terminated task is expected to free all resources and exit.
29
+ def terminated?
30
+ terminated
31
+ end
32
+
33
+ # Waits without timeout till the task executing thread is finished
34
+ def join
35
+ thread && thread.join
36
+ end
37
+
38
+ # Creates a new TaskSweeper
39
+ # * +timeout_period+: timeout value in seconds
40
+ def initialize(timeout_period)
41
+ self.timeout_period = timeout_period
42
+ self.terminated = false
43
+ self.last_ping = Time.now
44
+ end
45
+
46
+ protected
47
+
48
+ # Time of last ping
49
+ attr_accessor :last_ping
50
+
51
+ # Set to +true+ if the executed task has timed out
52
+ attr_accessor :terminated
53
+
54
+ # The task executing thread
55
+ attr_accessor :thread
56
+
57
+ # Executes the given block and times it out if stalled.
58
+ def timeout
59
+ exception = nil
60
+ self.thread = Thread.new do
61
+ begin
62
+ yield
63
+ rescue Exception => e
64
+ # save exception so it can be rethrown outside of the thread
65
+ exception = e
66
+ end
67
+ end
68
+ while self.thread.join(self.timeout_period) == nil do
69
+ if self.last_ping < Time.now - self.timeout_period
70
+ self.terminated = true
71
+ break
72
+ end
73
+ end
74
+ raise exception if exception
75
+ end
76
+ end
77
+ end
@@ -2,7 +2,7 @@ module RR #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1
4
4
  MINOR = 0
5
- TINY = 5
5
+ TINY = 6
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/rubyrep.rb CHANGED
@@ -42,15 +42,18 @@ require 'syncers/syncers'
42
42
  require 'syncers/two_way_syncer'
43
43
  require 'sync_runner'
44
44
  require 'trigger_mode_switcher'
45
+ require 'logged_change_loader'
45
46
  require 'logged_change'
46
47
  require 'replication_difference'
47
48
  require 'replication_helper'
48
49
  require 'replicators/replicators'
49
50
  require 'replicators/two_way_replicator'
51
+ require 'task_sweeper'
50
52
  require 'replication_run'
51
53
  require 'replication_runner'
52
54
  require 'uninstall_runner'
53
55
  require 'generate_runner'
56
+ require 'noisy_connection'
54
57
 
55
58
  Dir["#{File.dirname(__FILE__)}/rubyrep/connection_extenders/*.rb"].each do |extender|
56
59
  # jdbc_extender.rb is only loaded if we are running on jruby
@@ -0,0 +1,68 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ describe LoggedChangeLoaders do
6
+ before(:each) do
7
+ Initializer.configuration = standard_config
8
+ end
9
+
10
+ it "initializers should create both logged change loaders" do
11
+ session = Session.new
12
+ loaders = LoggedChangeLoaders.new(session)
13
+ loaders[:left].session.should == session
14
+ loaders[:left].database.should == :left
15
+ loaders[:right].database.should == :right
16
+ end
17
+
18
+ it "update should execute a forced update of both logged change loaders" do
19
+ session = Session.new
20
+ loaders = LoggedChangeLoaders.new(session)
21
+ loaders[:left].should_receive(:update).with(:forced => true)
22
+ loaders[:right].should_receive(:update).with(:forced => true)
23
+ loaders.update
24
+ end
25
+
26
+ end
27
+
28
+ describe LoggedChangeLoader do
29
+ before(:each) do
30
+ Initializer.configuration = standard_config
31
+ end
32
+
33
+ # Note:
34
+ # LoggedChangeLoader is a helper for LoggedChange.
35
+ # It is tested through the specs for LoggedChange.
36
+
37
+ it "oldest_change_time should return nil if there are no changes" do
38
+ session = Session.new
39
+ session.left.execute "delete from rr_pending_changes"
40
+ loader = LoggedChangeLoader.new session, :left
41
+ loader.oldest_change_time.should be_nil
42
+ end
43
+
44
+ it "oldest_change_time should return the time of the oldest change" do
45
+ session = Session.new
46
+ session.left.begin_db_transaction
47
+ begin
48
+ time = Time.now
49
+ session.left.insert_record 'rr_pending_changes', {
50
+ 'change_table' => 'left_table',
51
+ 'change_key' => 'id|1',
52
+ 'change_type' => 'I',
53
+ 'change_time' => time
54
+ }
55
+ session.left.insert_record 'rr_pending_changes', {
56
+ 'change_table' => 'left_table',
57
+ 'change_key' => 'id|2',
58
+ 'change_type' => 'I',
59
+ 'change_time' => 100.seconds.from_now
60
+ }
61
+ loader = LoggedChangeLoader.new session, :left
62
+ loader.oldest_change_time.should.to_s == time.to_s
63
+ ensure
64
+ session.left.rollback_db_transaction
65
+ end
66
+ end
67
+
68
+ end
@@ -9,7 +9,8 @@ describe LoggedChange do
9
9
 
10
10
  it "initialize should store session and database" do
11
11
  session = Session.new
12
- change = LoggedChange.new session, :left
12
+ loader = LoggedChangeLoader.new session, :left
13
+ change = LoggedChange.new loader
13
14
  change.session.should == session
14
15
  change.database.should == :left
15
16
  end
@@ -37,7 +38,8 @@ describe LoggedChange do
37
38
  'change_type' => 'I',
38
39
  'change_time' => Time.now
39
40
  }
40
- change = LoggedChange.new session, :left
41
+ loader = LoggedChangeLoader.new session, :left
42
+ change = LoggedChange.new loader
41
43
  change.load_specified 'left_table', {'id' => '2'}
42
44
 
43
45
  change.table.should == 'left_table'
@@ -62,7 +64,8 @@ describe LoggedChange do
62
64
  'change_type' => 'I',
63
65
  'change_time' => Time.now
64
66
  }
65
- change = LoggedChange.new session, :left
67
+ loader = LoggedChangeLoader.new session, :left
68
+ change = LoggedChange.new loader
66
69
  change.load_specified 'scanner_records', {'id1' => 1, 'id2' => 2}
67
70
 
68
71
  change.table.should == 'scanner_records'
@@ -83,7 +86,8 @@ describe LoggedChange do
83
86
  'change_type' => 'I',
84
87
  'change_time' => Time.now
85
88
  }
86
- change = LoggedChange.new session, :left
89
+ loader = LoggedChangeLoader.new session, :left
90
+ change = LoggedChange.new loader
87
91
  change.load_specified 'left_table', {'id' => 1}
88
92
 
89
93
  session.left.
@@ -113,7 +117,8 @@ describe LoggedChange do
113
117
  'change_type' => 'U',
114
118
  'change_time' => t2
115
119
  }
116
- change = LoggedChange.new session, :left
120
+ loader = LoggedChangeLoader.new session, :left
121
+ change = LoggedChange.new loader
117
122
  change.load_specified 'left_table', {'id' => 1}
118
123
 
119
124
  change.first_changed_at.to_s.should == t1.to_s
@@ -141,7 +146,8 @@ describe LoggedChange do
141
146
  'change_type' => 'U',
142
147
  'change_time' => Time.now
143
148
  }
144
- change = LoggedChange.new session, :left
149
+ loader = LoggedChangeLoader.new session, :left
150
+ change = LoggedChange.new loader
145
151
  change.load_specified 'left_table', {'id' => 1}
146
152
 
147
153
  change.type.should == :update
@@ -175,7 +181,8 @@ describe LoggedChange do
175
181
  'change_type' => 'D',
176
182
  'change_time' => Time.now
177
183
  }
178
- change = LoggedChange.new session, :left
184
+ loader = LoggedChangeLoader.new session, :left
185
+ change = LoggedChange.new loader
179
186
  change.load_specified 'left_table', {'id' => '1'}
180
187
 
181
188
  change.type.should == :no_change
@@ -215,7 +222,8 @@ describe LoggedChange do
215
222
  'change_type' => 'U',
216
223
  'change_time' => Time.now
217
224
  }
218
- change = LoggedChange.new session, :left
225
+ loader = LoggedChangeLoader.new session, :left
226
+ change = LoggedChange.new loader
219
227
  change.load_specified 'left_table', {'id' => '1'}
220
228
  change.type.should == :insert
221
229
  change.key.should == {'id' => '2'}
@@ -233,8 +241,8 @@ describe LoggedChange do
233
241
  'change_type' => 'I',
234
242
  'change_time' => Time.now
235
243
  }
236
- session.reload_changes
237
- change = LoggedChange.new session, :left
244
+ loader.update :forced => true
245
+ change = LoggedChange.new loader
238
246
  change.load_specified 'left_table', {'id' => '5'}
239
247
  change.type.should == :update
240
248
  change.key.should == {'id' => '5'}
@@ -254,7 +262,8 @@ describe LoggedChange do
254
262
  'change_type' => 'I',
255
263
  'change_time' => Time.now
256
264
  }
257
- change = LoggedChange.new session, :left
265
+ loader = LoggedChangeLoader.new session, :left
266
+ change = LoggedChange.new loader
258
267
  change.load_specified 'scanner_records', {'id' => '1'}
259
268
 
260
269
  change.table.should == 'scanner_records'
@@ -275,7 +284,8 @@ describe LoggedChange do
275
284
  session = Session.new
276
285
  session.left.begin_db_transaction
277
286
  begin
278
- change = LoggedChange.new session, :left
287
+ loader = LoggedChangeLoader.new session, :left
288
+ change = LoggedChange.new loader
279
289
  change.load_specified 'scanner_records', {'id' => '1'}
280
290
 
281
291
  change.table.should == 'scanner_records'
@@ -307,7 +317,8 @@ describe LoggedChange do
307
317
  'change_type' => 'U',
308
318
  'change_time' => Time.now
309
319
  }
310
- change = LoggedChange.new session, :left
320
+ loader = LoggedChangeLoader.new session, :left
321
+ change = LoggedChange.new loader
311
322
  change.load_specified 'left_table', {'id' => '1'}
312
323
  session.left.insert_record 'rr_pending_changes', {
313
324
  'change_table' => 'left_table',
@@ -315,7 +326,7 @@ describe LoggedChange do
315
326
  'change_type' => 'D',
316
327
  'change_time' => Time.now
317
328
  }
318
- session.reload_changes
329
+ loader.update :forced => true
319
330
  change.load
320
331
 
321
332
  change.table.should == 'left_table'
@@ -341,7 +352,8 @@ describe LoggedChange do
341
352
  'change_type' => 'U',
342
353
  'change_time' => Time.now
343
354
  }
344
- change = LoggedChange.new session, :left
355
+ loader = LoggedChangeLoader.new session, :left
356
+ change = LoggedChange.new loader
345
357
  change.load_specified 'left_table', {'id' => '1'}
346
358
  session.left.insert_record 'rr_pending_changes', {
347
359
  'change_table' => 'left_table',
@@ -350,7 +362,7 @@ describe LoggedChange do
350
362
  'change_type' => 'U',
351
363
  'change_time' => Time.now
352
364
  }
353
- session.reload_changes
365
+ loader.update :forced => true
354
366
  change.load
355
367
 
356
368
  change.table.should == 'left_table'
@@ -362,39 +374,9 @@ describe LoggedChange do
362
374
  end
363
375
  end
364
376
 
365
- it "oldest_change_time should return nil if there are no changes" do
366
- session = Session.new
367
- session.left.execute "delete from rr_pending_changes"
368
- change = LoggedChange.new session, :left
369
- change.oldest_change_time.should be_nil
370
- end
371
-
372
- it "oldest_change_time should return the time of the oldest change" do
373
- session = Session.new
374
- session.left.begin_db_transaction
375
- begin
376
- time = Time.now
377
- session.left.insert_record 'rr_pending_changes', {
378
- 'change_table' => 'left_table',
379
- 'change_key' => 'id|1',
380
- 'change_type' => 'I',
381
- 'change_time' => time
382
- }
383
- session.left.insert_record 'rr_pending_changes', {
384
- 'change_table' => 'left_table',
385
- 'change_key' => 'id|2',
386
- 'change_type' => 'I',
387
- 'change_time' => 100.seconds.from_now
388
- }
389
- change = LoggedChange.new session, :left
390
- change.oldest_change_time.should.to_s == time.to_s
391
- ensure
392
- session.left.rollback_db_transaction
393
- end
394
- end
395
-
396
377
  it "key_from_raw_key should return the correct column_name => value hash for the given key" do
397
- change = LoggedChange.new Session.new, :left
378
+ loader = LoggedChangeLoader.new Session.new, :left
379
+ change = LoggedChange.new loader
398
380
  change.key_to_hash("a|1|b|2").should == {
399
381
  'a' => '1',
400
382
  'b' => '2'
@@ -402,7 +384,8 @@ describe LoggedChange do
402
384
  end
403
385
 
404
386
  it "key_from_raw_key should work with multi character key_sep strings" do
405
- change = LoggedChange.new Session.new, :left
387
+ loader = LoggedChangeLoader.new Session.new, :left
388
+ change = LoggedChange.new loader
406
389
  change.stub!(:key_sep).and_return('BLA')
407
390
  change.key_to_hash("aBLA1BLAbBLA2").should == {
408
391
  'a' => '1',
@@ -411,7 +394,8 @@ describe LoggedChange do
411
394
  end
412
395
 
413
396
  it "load_oldest should not load a change if none available" do
414
- change = LoggedChange.new Session.new, :left
397
+ loader = LoggedChangeLoader.new Session.new, :left
398
+ change = LoggedChange.new loader
415
399
  change.should_not_receive :load_specified
416
400
  change.load_oldest
417
401
  end
@@ -432,7 +416,8 @@ describe LoggedChange do
432
416
  'change_type' => 'I',
433
417
  'change_time' => Time.now
434
418
  }
435
- change = LoggedChange.new session, :left
419
+ loader = LoggedChangeLoader.new session, :left
420
+ change = LoggedChange.new loader
436
421
  change.load_oldest
437
422
 
438
423
  change.key.should == {'id' => '1'}
@@ -463,7 +448,8 @@ describe LoggedChange do
463
448
  'change_type' => 'I',
464
449
  'change_time' => Time.now
465
450
  }
466
- change = LoggedChange.new session, :left
451
+ loader = LoggedChangeLoader.new session, :left
452
+ change = LoggedChange.new loader
467
453
  change.load_oldest
468
454
 
469
455
  change.type.should == :insert
@@ -473,8 +459,12 @@ describe LoggedChange do
473
459
  end
474
460
  end
475
461
 
476
- it "to_yaml should blank out session" do
477
- change = LoggedChange.new :dummy_session, :left
478
- change.to_yaml.should_not =~ /session/
462
+ it "to_yaml should blank out session and loader" do
463
+ session = Session.new
464
+ loader = LoggedChangeLoader.new session, :left
465
+ change = LoggedChange.new loader
466
+ yaml = change.to_yaml
467
+ yaml.should_not =~ /session/
468
+ yaml.should_not =~ /loader/
479
469
  end
480
470
  end