rubyrep 1.0.5 → 1.0.6

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