offroad 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/LICENSE +674 -674
  2. data/README.rdoc +29 -29
  3. data/Rakefile +75 -75
  4. data/TODO +42 -42
  5. data/lib/app/models/offroad/group_state.rb +85 -85
  6. data/lib/app/models/offroad/mirror_info.rb +53 -53
  7. data/lib/app/models/offroad/model_state.rb +36 -36
  8. data/lib/app/models/offroad/received_record_state.rb +115 -115
  9. data/lib/app/models/offroad/sendable_record_state.rb +91 -91
  10. data/lib/app/models/offroad/system_state.rb +33 -33
  11. data/lib/cargo_streamer.rb +222 -222
  12. data/lib/controller_extensions.rb +74 -74
  13. data/lib/exceptions.rb +16 -16
  14. data/lib/migrate/20100512164608_create_offroad_tables.rb +72 -72
  15. data/lib/mirror_data.rb +376 -376
  16. data/lib/model_extensions.rb +378 -377
  17. data/lib/module_funcs.rb +94 -94
  18. data/lib/offroad.rb +41 -41
  19. data/lib/version.rb +3 -3
  20. data/lib/view_helper.rb +7 -7
  21. data/templates/offline.rb +36 -36
  22. data/templates/offline_database.yml +7 -7
  23. data/templates/offroad.yml +6 -6
  24. data/test/app_root/app/controllers/application_controller.rb +2 -2
  25. data/test/app_root/app/controllers/group_controller.rb +28 -28
  26. data/test/app_root/app/models/global_record.rb +10 -10
  27. data/test/app_root/app/models/group.rb +12 -12
  28. data/test/app_root/app/models/group_owned_record.rb +68 -68
  29. data/test/app_root/app/models/guest.rb +7 -7
  30. data/test/app_root/app/models/subrecord.rb +12 -12
  31. data/test/app_root/app/models/unmirrored_record.rb +4 -4
  32. data/test/app_root/app/views/group/download_down_mirror.html.erb +3 -3
  33. data/test/app_root/app/views/group/download_initial_down_mirror.html.erb +3 -3
  34. data/test/app_root/app/views/group/download_up_mirror.html.erb +5 -5
  35. data/test/app_root/app/views/layouts/mirror.html.erb +8 -8
  36. data/test/app_root/config/boot.rb +115 -115
  37. data/test/app_root/config/database-pg.yml +8 -8
  38. data/test/app_root/config/database.yml +5 -5
  39. data/test/app_root/config/environment.rb +24 -24
  40. data/test/app_root/config/environments/test.rb +17 -17
  41. data/test/app_root/config/offroad.yml +6 -6
  42. data/test/app_root/config/routes.rb +4 -4
  43. data/test/app_root/db/migrate/20100529235049_create_tables.rb +64 -64
  44. data/test/app_root/lib/common_hobo.rb +15 -15
  45. data/test/app_root/vendor/plugins/offroad/init.rb +2 -2
  46. data/test/functional/mirror_operations_test.rb +148 -148
  47. data/test/test_helper.rb +453 -453
  48. data/test/unit/app_state_tracking_test.rb +275 -275
  49. data/test/unit/cargo_streamer_test.rb +332 -332
  50. data/test/unit/global_data_test.rb +102 -102
  51. data/test/unit/group_controller_test.rb +152 -152
  52. data/test/unit/group_data_test.rb +442 -435
  53. data/test/unit/group_single_test.rb +136 -136
  54. data/test/unit/hobo_permissions_test.rb +57 -57
  55. data/test/unit/mirror_data_test.rb +1283 -1283
  56. data/test/unit/mirror_info_test.rb +31 -31
  57. data/test/unit/module_funcs_test.rb +37 -37
  58. data/test/unit/pathological_model_test.rb +62 -62
  59. data/test/unit/test_framework_test.rb +86 -86
  60. data/test/unit/unmirrored_data_test.rb +14 -14
  61. metadata +6 -8
@@ -1,74 +1,74 @@
1
- module Offroad
2
- module ControllerExtensions
3
- def offroad_group_controller
4
- include InstanceMethods
5
- end
6
-
7
- module InstanceMethods
8
- def render_up_mirror_file(group, filename, render_args = {})
9
- ensure_group_offline(group)
10
- raise PluginError.new("Cannot generate up-mirror file when app in online mode") if Offroad::app_online?
11
- mirror_data = MirrorData.new(
12
- group,
13
- :skip_validation => render_args.delete(:skip_validation)
14
- )
15
- render_appending_mirror_data(group, filename, render_args) do |output|
16
- mirror_data.write_upwards_data(output)
17
- end
18
- end
19
-
20
- def render_down_mirror_file(group, filename, render_args = {})
21
- ensure_group_offline(group)
22
- raise PluginError.new("Cannot generate down-mirror file when app in offline mode") if Offroad::app_offline?
23
- mirror_data = MirrorData.new(
24
- group,
25
- :initial_mode => render_args.delete(:initial_mode),
26
- :skip_validation => render_args.delete(:skip_validation)
27
- )
28
- render_appending_mirror_data(group, filename, render_args) do |output|
29
- mirror_data.write_downwards_data(output)
30
- end
31
- end
32
-
33
- def load_up_mirror_file(group, data, options = {})
34
- ensure_group_offline(group)
35
- raise PluginError.new("Cannot accept up mirror file when app is in offline mode") if Offroad::app_offline?
36
- m = MirrorData.new(
37
- group,
38
- :skip_validation => options[:skip_validation]
39
- )
40
- m.load_upwards_data(data)
41
- end
42
-
43
- def load_down_mirror_file(group, data, options = {})
44
- ensure_group_offline(group) if group
45
- raise PluginError.new("Cannot accept down mirror file when app is in online mode") if Offroad::app_online?
46
- m = MirrorData.new(
47
- group,
48
- :initial_mode => options[:initial_mode],
49
- :skip_validation => options[:skip_validation]
50
- )
51
- m.load_downwards_data(data)
52
- end
53
-
54
- private
55
-
56
- def ensure_group_offline(group)
57
- raise PluginError.new("Cannot perform mirror operations on online group") unless group.group_offline?
58
- end
59
-
60
- def render_appending_mirror_data(group, filename, render_args)
61
- # Encourage browser to download this to disk instead of displaying it
62
- headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
63
- viewable_content = render_to_string render_args
64
-
65
- render_proc = Proc.new do |response, output|
66
- output.write(viewable_content)
67
- yield output
68
- end
69
-
70
- render :text => render_proc
71
- end
72
- end
73
- end
74
- end
1
+ module Offroad
2
+ module ControllerExtensions
3
+ def offroad_group_controller
4
+ include InstanceMethods
5
+ end
6
+
7
+ module InstanceMethods
8
+ def render_up_mirror_file(group, filename, render_args = {})
9
+ ensure_group_offline(group)
10
+ raise PluginError.new("Cannot generate up-mirror file when app in online mode") if Offroad::app_online?
11
+ mirror_data = MirrorData.new(
12
+ group,
13
+ :skip_validation => render_args.delete(:skip_validation)
14
+ )
15
+ render_appending_mirror_data(group, filename, render_args) do |output|
16
+ mirror_data.write_upwards_data(output)
17
+ end
18
+ end
19
+
20
+ def render_down_mirror_file(group, filename, render_args = {})
21
+ ensure_group_offline(group)
22
+ raise PluginError.new("Cannot generate down-mirror file when app in offline mode") if Offroad::app_offline?
23
+ mirror_data = MirrorData.new(
24
+ group,
25
+ :initial_mode => render_args.delete(:initial_mode),
26
+ :skip_validation => render_args.delete(:skip_validation)
27
+ )
28
+ render_appending_mirror_data(group, filename, render_args) do |output|
29
+ mirror_data.write_downwards_data(output)
30
+ end
31
+ end
32
+
33
+ def load_up_mirror_file(group, data, options = {})
34
+ ensure_group_offline(group)
35
+ raise PluginError.new("Cannot accept up mirror file when app is in offline mode") if Offroad::app_offline?
36
+ m = MirrorData.new(
37
+ group,
38
+ :skip_validation => options[:skip_validation]
39
+ )
40
+ m.load_upwards_data(data)
41
+ end
42
+
43
+ def load_down_mirror_file(group, data, options = {})
44
+ ensure_group_offline(group) if group
45
+ raise PluginError.new("Cannot accept down mirror file when app is in online mode") if Offroad::app_online?
46
+ m = MirrorData.new(
47
+ group,
48
+ :initial_mode => options[:initial_mode],
49
+ :skip_validation => options[:skip_validation]
50
+ )
51
+ m.load_downwards_data(data)
52
+ end
53
+
54
+ private
55
+
56
+ def ensure_group_offline(group)
57
+ raise PluginError.new("Cannot perform mirror operations on online group") unless group.group_offline?
58
+ end
59
+
60
+ def render_appending_mirror_data(group, filename, render_args)
61
+ # Encourage browser to download this to disk instead of displaying it
62
+ headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
63
+ viewable_content = render_to_string render_args
64
+
65
+ render_proc = Proc.new do |response, output|
66
+ output.write(viewable_content)
67
+ yield output
68
+ end
69
+
70
+ render :text => render_proc
71
+ end
72
+ end
73
+ end
74
+ end
data/lib/exceptions.rb CHANGED
@@ -1,16 +1,16 @@
1
- module Offroad
2
- class DataError < RuntimeError
3
- end
4
-
5
- class OldDataError < DataError
6
- end
7
-
8
- class ModelError < RuntimeError
9
- end
10
-
11
- class PluginError < RuntimeError
12
- end
13
-
14
- class AppModeUnknownError < RuntimeError
15
- end
16
- end
1
+ module Offroad
2
+ class DataError < RuntimeError
3
+ end
4
+
5
+ class OldDataError < DataError
6
+ end
7
+
8
+ class ModelError < RuntimeError
9
+ end
10
+
11
+ class PluginError < RuntimeError
12
+ end
13
+
14
+ class AppModeUnknownError < RuntimeError
15
+ end
16
+ end
@@ -1,72 +1,72 @@
1
- class CreateOffroadTables < ActiveRecord::Migration
2
- def self.up
3
- create_table :offroad_system_state do |t|
4
- t.column :current_mirror_version, :integer
5
- end
6
-
7
- create_table :offroad_group_states do |t|
8
- t.column :app_group_id, :integer, :null => false
9
-
10
- # This is used to allow group_owned records to be destroyed when their parent is.
11
- # Without this, groups with :dependent => :destroy would not be allowed on the online app.
12
- # This is NOT used to propogate group deletion through mirror files.
13
- t.column :group_being_destroyed, :boolean, :default => false, :null => false
14
-
15
- # If a group is locked (which can only occur offline) then its records cannot be altered anymore.
16
- # If a locked group is sent up to the online system, then after loading it the online system puts that group back
17
- # online.
18
- t.column :group_locked, :boolean, :default => false, :null => false
19
-
20
- # On both the online and offline systems, these are the latest data versions remote side is known to have
21
- t.column :confirmed_group_data_version, :integer, :null => false
22
- t.column :confirmed_global_data_version, :integer, :null => false
23
-
24
- t.column :last_installer_downloaded_at, :datetime
25
- t.column :last_installation_at, :datetime
26
- t.column :last_down_mirror_created_at, :datetime
27
- t.column :last_down_mirror_loaded_at, :datetime
28
- t.column :last_up_mirror_created_at, :datetime
29
- t.column :last_up_mirror_loaded_at, :datetime
30
- t.column :launcher_version, :integer
31
- t.column :app_version, :integer
32
- t.column :operating_system, :string, :default => "Unknown", :null => false
33
- end
34
- add_index :offroad_group_states, :app_group_id, :unique => true
35
- # This lets us quickly find min(global_mirror_version) for clearing old deleted global record SRSes
36
- add_index :offroad_group_states, :confirmed_global_data_version
37
-
38
- create_table :offroad_model_states do |t|
39
- t.column :app_model_name, :string, :null => false
40
- end
41
- add_index :offroad_model_states, :app_model_name, :unique => true
42
-
43
- create_table :offroad_sendable_record_states do |t|
44
- t.column :model_state_id, :integer, :null => false
45
- t.column :local_record_id, :integer, :null => false
46
- t.column :mirror_version, :integer, :default => 0, :null => false
47
- t.column :deleted, :boolean, :default => false, :null => false
48
- end
49
- # This index is for locating the SRS for any given local app record
50
- add_index :offroad_sendable_record_states, [:local_record_id, :model_state_id], :unique => true
51
- # This index is for generating mirror files: for a given model need to find everything above a given mirror_version
52
- add_index :offroad_sendable_record_states, [:model_state_id, :deleted, :mirror_version]
53
-
54
- create_table :offroad_received_record_states do |t|
55
- t.column :model_state_id, :integer, :null => false
56
- t.column :group_state_id, :integer, :default => 0, :null => false # If 0, is a global record
57
- t.column :local_record_id, :integer, :null => false
58
- t.column :remote_record_id, :integer, :null => false
59
- end
60
- add_index :offroad_received_record_states, [:model_state_id, :group_state_id, :remote_record_id], :unique => true
61
- # TODO: Perhaps index below can be removed; it enforces data integrity, but isn't actually used for lookups
62
- add_index :offroad_received_record_states, [:model_state_id, :local_record_id], :unique => true
63
- end
64
-
65
- def self.down
66
- drop_table :offroad_system_state
67
- drop_table :offroad_group_states
68
- drop_table :offroad_model_states
69
- drop_table :offroad_sendable_record_states
70
- drop_table :offroad_received_record_states
71
- end
72
- end
1
+ class CreateOffroadTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :offroad_system_state do |t|
4
+ t.column :current_mirror_version, :integer
5
+ end
6
+
7
+ create_table :offroad_group_states do |t|
8
+ t.column :app_group_id, :integer, :null => false
9
+
10
+ # This is used to allow group_owned records to be destroyed when their parent is.
11
+ # Without this, groups with :dependent => :destroy would not be allowed on the online app.
12
+ # This is NOT used to propogate group deletion through mirror files.
13
+ t.column :group_being_destroyed, :boolean, :default => false, :null => false
14
+
15
+ # If a group is locked (which can only occur offline) then its records cannot be altered anymore.
16
+ # If a locked group is sent up to the online system, then after loading it the online system puts that group back
17
+ # online.
18
+ t.column :group_locked, :boolean, :default => false, :null => false
19
+
20
+ # On both the online and offline systems, these are the latest data versions remote side is known to have
21
+ t.column :confirmed_group_data_version, :integer, :null => false
22
+ t.column :confirmed_global_data_version, :integer, :null => false
23
+
24
+ t.column :last_installer_downloaded_at, :datetime
25
+ t.column :last_installation_at, :datetime
26
+ t.column :last_down_mirror_created_at, :datetime
27
+ t.column :last_down_mirror_loaded_at, :datetime
28
+ t.column :last_up_mirror_created_at, :datetime
29
+ t.column :last_up_mirror_loaded_at, :datetime
30
+ t.column :launcher_version, :integer
31
+ t.column :app_version, :integer
32
+ t.column :operating_system, :string, :default => "Unknown", :null => false
33
+ end
34
+ add_index :offroad_group_states, :app_group_id, :unique => true
35
+ # This lets us quickly find min(global_mirror_version) for clearing old deleted global record SRSes
36
+ add_index :offroad_group_states, :confirmed_global_data_version
37
+
38
+ create_table :offroad_model_states do |t|
39
+ t.column :app_model_name, :string, :null => false
40
+ end
41
+ add_index :offroad_model_states, :app_model_name, :unique => true
42
+
43
+ create_table :offroad_sendable_record_states do |t|
44
+ t.column :model_state_id, :integer, :null => false
45
+ t.column :local_record_id, :integer, :null => false
46
+ t.column :mirror_version, :integer, :default => 0, :null => false
47
+ t.column :deleted, :boolean, :default => false, :null => false
48
+ end
49
+ # This index is for locating the SRS for any given local app record
50
+ add_index :offroad_sendable_record_states, [:local_record_id, :model_state_id], :unique => true
51
+ # This index is for generating mirror files: for a given model need to find everything above a given mirror_version
52
+ add_index :offroad_sendable_record_states, [:model_state_id, :deleted, :mirror_version]
53
+
54
+ create_table :offroad_received_record_states do |t|
55
+ t.column :model_state_id, :integer, :null => false
56
+ t.column :group_state_id, :integer, :default => 0, :null => false # If 0, is a global record
57
+ t.column :local_record_id, :integer, :null => false
58
+ t.column :remote_record_id, :integer, :null => false
59
+ end
60
+ add_index :offroad_received_record_states, [:model_state_id, :group_state_id, :remote_record_id], :unique => true
61
+ # TODO: Perhaps index below can be removed; it enforces data integrity, but isn't actually used for lookups
62
+ add_index :offroad_received_record_states, [:model_state_id, :local_record_id], :unique => true
63
+ end
64
+
65
+ def self.down
66
+ drop_table :offroad_system_state
67
+ drop_table :offroad_group_states
68
+ drop_table :offroad_model_states
69
+ drop_table :offroad_sendable_record_states
70
+ drop_table :offroad_received_record_states
71
+ end
72
+ end
data/lib/mirror_data.rb CHANGED
@@ -1,376 +1,376 @@
1
- module Offroad
2
- private
3
-
4
- class MirrorData
5
- attr_reader :group, :mode
6
-
7
- def initialize(group, options = {})
8
- @group = group
9
- @initial_mode = options.delete(:initial_mode) || false
10
- @skip_validation = options.delete(:skip_validation) || false
11
-
12
- raise PluginError.new("Invalid option keys") unless options.size == 0
13
-
14
- unless Offroad::app_offline? && @initial_mode
15
- raise PluginError.new("Need group") unless @group.is_a?(Offroad::group_base_model) && !@group.new_record?
16
- raise DataError.new("Group must be in offline mode") unless @group.group_offline?
17
- end
18
-
19
- @imported_models_to_validate = []
20
- end
21
-
22
- def write_upwards_data(tgt = nil)
23
- raise PluginError.new("Can only write upwards data in offline mode") unless Offroad.app_offline?
24
- raise PluginError.new("No such thing as initial upwards data") if @initial_mode
25
- write_data(tgt) do |cs|
26
- add_group_specific_cargo(cs)
27
- end
28
- end
29
-
30
- def write_downwards_data(tgt = nil)
31
- raise PluginError.new("Can only write downwards data in online mode") unless Offroad.app_online?
32
- write_data(tgt) do |cs|
33
- add_global_cargo(cs)
34
- if @initial_mode
35
- add_group_specific_cargo(cs)
36
- end
37
- end
38
- end
39
-
40
- def load_upwards_data(src)
41
- raise PluginError.new("Can only load upwards data in online mode") unless Offroad.app_online?
42
- raise PluginError.new("No such thing as initial upwards data") if @initial_mode
43
-
44
- read_data_from("offline", src) do |cs, mirror_info, cargo_group_state|
45
- unless cargo_group_state.confirmed_group_data_version > @group.group_state.confirmed_group_data_version
46
- raise OldDataError.new("File contains old up-mirror data")
47
- end
48
- import_group_specific_cargo(cs)
49
- @group.group_offline = false if cargo_group_state.group_locked?
50
- end
51
- end
52
-
53
- def load_downwards_data(src)
54
- raise PluginError.new("Can only load downwards data in offline mode") unless Offroad.app_offline?
55
-
56
- read_data_from("online", src) do |cs, mirror_info, cargo_group_state|
57
- raise DataError.new("Unexpected initial file value") unless mirror_info.initial_file == @initial_mode
58
-
59
- group_cargo_name = MirrorData::data_cargo_name_for_model(Offroad::group_base_model)
60
- if mirror_info.initial_file
61
- raise DataError.new("No group data in initial down mirror file") unless cs.has_cargo_named?(group_cargo_name)
62
- # This is an initial mirror file, so we want it to determine the entirety of the database's new state
63
- # However, existing data is safe if there's a mid-import error; read_data_from places us in a transaction
64
- delete_all_existing_database_records!
65
-
66
- import_global_cargo(cs) # Global cargo must be done first because group data might belong_to global data
67
- import_group_specific_cargo(cs)
68
- else
69
- # Regular, non-initial down mirror file
70
- unless cargo_group_state.confirmed_global_data_version > @group.group_state.confirmed_global_data_version
71
- raise OldDataError.new("File contains old down-mirror data")
72
- end
73
- import_global_cargo(cs)
74
- end
75
-
76
- # Load information into our group state that the online app is in a better position to know about
77
- @group = Offroad::offline_group if @initial_mode
78
- end
79
- end
80
-
81
- private
82
-
83
- def self.data_cargo_name_for_model(model)
84
- "data_#{model.name}"
85
- end
86
-
87
- def self.deletion_cargo_name_for_model(model)
88
- "deletion_#{model.name}"
89
- end
90
-
91
- def delete_all_existing_database_records!
92
- tables = ActiveRecord::Base.connection.tables
93
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
94
- # Emptying sqlite_sequence resets SQLite's autoincrement counters.
95
- # SQLite's autoincrement is nice in that it automatically picks largest ever id + 1.
96
- # This means that after clearing sqlite_sequence and then populating database with manually-id'd rows,
97
- # new records will be inserted with unique id's, no problem.
98
- tables << "sqlite_sequence"
99
- end
100
-
101
- tables.each do |table|
102
- next if table.start_with?("virtual_") # Used in testing # FIXME Should pick something less likely to collide with app name
103
- next if table == "schema_migrations"
104
- ActiveRecord::Base.connection.execute "DELETE FROM #{table}"
105
- end
106
-
107
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
108
- # Reset all sequences so that autoincremented ids start from 1 again
109
- seqnames = ActiveRecord::Base.connection.select_values "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
110
- seqnames.each do |s|
111
- ActiveRecord::Base.connection.execute "SELECT setval('#{s}', 1, false)"
112
- end
113
- end
114
- end
115
-
116
- def write_data(tgt)
117
- cs = nil
118
- temp_sio = nil
119
- case tgt
120
- when CargoStreamer
121
- cs = tgt
122
- when nil
123
- temp_sio = StringIO.new("", "w")
124
- cs = CargoStreamer.new(temp_sio, "w")
125
- else
126
- cs = CargoStreamer.new(tgt, "w")
127
- end
128
-
129
- # TODO: Figure out if this transaction ensures we get a consistent read state
130
- Offroad::group_base_model.connection.transaction do
131
- Offroad::group_base_model.cache do
132
- begin
133
- mirror_info = MirrorInfo.new_from_group(@group, @initial_mode)
134
- cs.write_cargo_section("mirror_info", [mirror_info], :human_readable => true)
135
-
136
- group_state = @group.group_state
137
- if Offroad::app_online?
138
- # Let the offline app know what global data version it's being updated to
139
- group_state.confirmed_global_data_version = SystemState::current_mirror_version
140
- else
141
- # Let the online app know what group data version the online mirror of this group is being updated to
142
- group_state.confirmed_group_data_version = SystemState::current_mirror_version
143
- end
144
- cs.write_cargo_section("group_state", [group_state], :human_readable => true)
145
-
146
- yield cs
147
-
148
- SystemState::increment_mirror_version
149
- rescue Offroad::CargoStreamerError
150
- raise Offroad::DataError.new("Encountered data validation error while writing to cargo file")
151
- end
152
- end
153
- end
154
-
155
- return temp_sio.string if temp_sio
156
- end
157
-
158
- def read_data_from(expected_source_app_mode, src)
159
- cs = case src
160
- when CargoStreamer then src
161
- when String then CargoStreamer.new(StringIO.new(src, "r"), "r")
162
- else CargoStreamer.new(src, "r")
163
- end
164
-
165
- raise DataError.new("Invalid mirror file, no info section found") unless cs.has_cargo_named?("mirror_info")
166
- mirror_info = cs.first_cargo_element("mirror_info")
167
- raise DataError.new("Invalid info section type") unless mirror_info.is_a?(MirrorInfo)
168
- unless mirror_info.app_mode.downcase == expected_source_app_mode.downcase
169
- raise DataError.new "Mirror file was generated by app in wrong mode; was expecting #{expected_source_app_mode}"
170
- end
171
-
172
- raise DataError.new("Invalid mirror file, no group state found") unless cs.has_cargo_named?("group_state")
173
- group_state = cs.first_cargo_element("group_state")
174
- raise DataError.new("Invalid group state type") unless group_state.is_a?(GroupState)
175
- group_state.readonly!
176
-
177
- # FIXME: Is this transaction call helping at all?
178
- Offroad::group_base_model.connection.transaction do
179
- Offroad::group_base_model.cache do
180
- yield cs, mirror_info, group_state
181
- validate_imported_models(cs) unless @skip_validation
182
-
183
- # Load information into our group state that the remote app is in a better position to know about
184
- @group.group_state.update_from_remote_group_state!(group_state) if @group && @group.group_offline?
185
- end
186
- end
187
-
188
- SystemState::increment_mirror_version if @initial_mode
189
- end
190
-
191
- def add_group_specific_cargo(cs)
192
- Offroad::group_owned_models.each do |name, model|
193
- add_model_cargo(cs, model)
194
- end
195
- Offroad::group_single_models.each do |name, model|
196
- add_model_cargo(cs, model)
197
- end
198
- add_model_cargo(cs, Offroad::group_base_model)
199
- end
200
-
201
- def add_global_cargo(cs)
202
- Offroad::global_data_models.each do |name, model|
203
- add_model_cargo(cs, model)
204
- end
205
- end
206
-
207
- def add_model_cargo(cs, model)
208
- if @initial_mode
209
- add_initial_model_cargo(cs, model)
210
- else
211
- add_non_initial_model_cargo(cs, model)
212
- end
213
- end
214
-
215
- def add_initial_model_cargo(cs, model)
216
- # Include the data for relevant records in this model
217
- data_source = model
218
- data_source = data_source.owned_by_offroad_group(@group) if model.offroad_group_data? && @group
219
- data_source.find_in_batches(:batch_size => 100) do |batch|
220
- cs.write_cargo_section(
221
- MirrorData::data_cargo_name_for_model(model),
222
- batch,
223
- :skip_validation => @skip_validation
224
- )
225
-
226
- if model.offroad_group_data?
227
- # In initial mode the remote app will create records with the same id's as the corresponding records here
228
- # So we'll create RRSes indicating that we've already "received" the data we're about to send
229
- # Later when the remote app sends new information on those records, we'll know which ones it means
230
- rrs_source = Offroad::ReceivedRecordState.for_model_and_group_if_apropos(model, @group)
231
- existing_rrs = rrs_source.all(:conditions => {:remote_record_id => batch.map(&:id)}).index_by(&:remote_record_id)
232
- new_rrs = batch.reject{|r| existing_rrs.has_key?(r.id)}.map{|r| rrs_source.for_record(r).new(:remote_record_id => r.id)}
233
- if new_rrs.size > 0
234
- Offroad::ReceivedRecordState.import(new_rrs, :validate => false, :timestamps => false)
235
- end
236
- end
237
- end
238
- end
239
-
240
- def add_non_initial_model_cargo(cs, model)
241
- # Include the data for relevant records in this model that are newer than the remote side's known latest version
242
- gs = @group.group_state
243
- remote_version = nil
244
- if model.offroad_group_data?
245
- remote_version = gs.confirmed_group_data_version
246
- else
247
- remote_version = gs.confirmed_global_data_version
248
- end
249
- srs_source = SendableRecordState.for_model(model).with_version_greater_than(remote_version)
250
- srs_source.for_non_deleted_records.find_in_batches(:batch_size => 100) do |srs_batch|
251
- # TODO Might be able to optimize this to one query using a join on app model and SRS tables
252
- record_ids = srs_batch.map { |srs| srs.local_record_id }
253
- data_batch = model.find(:all, :conditions => {:id => record_ids})
254
- raise PluginError.new("Invalid SRS ids") if data_batch.size != srs_batch.size
255
- cs.write_cargo_section(
256
- MirrorData::data_cargo_name_for_model(model),
257
- data_batch,
258
- :skip_validation => @skip_validation
259
- )
260
- end
261
-
262
- # Also need to include information about records that have been destroyed
263
- srs_source.for_deleted_records.find_in_batches(:batch_size => 100) do |deletion_batch|
264
- cs.write_cargo_section(MirrorData::deletion_cargo_name_for_model(model), deletion_batch)
265
- end
266
- end
267
-
268
- def import_group_specific_cargo(cs)
269
- import_model_cargo(cs, Offroad::group_base_model)
270
- Offroad::group_owned_models.each do |name, model|
271
- import_model_cargo(cs, model)
272
- end
273
- Offroad::group_single_models.each do |name, model|
274
- import_model_cargo(cs, model)
275
- end
276
- end
277
-
278
- def import_global_cargo(cs)
279
- Offroad::global_data_models.each do |name, model|
280
- import_model_cargo(cs, model)
281
- end
282
- end
283
-
284
- def import_model_cargo(cs, model)
285
- @imported_models_to_validate.push model
286
-
287
- if @initial_mode && model.offroad_group_data?
288
- import_initial_model_cargo(cs, model)
289
- else
290
- import_non_initial_model_cargo(cs, model)
291
- end
292
- end
293
-
294
- def import_initial_model_cargo(cs, model)
295
- cs.each_cargo_section(MirrorData::data_cargo_name_for_model(model)) do |batch|
296
- # Notice we are using the same primary key values as the online system, not allocating new ones
297
- model.import batch, :validate => false, :timestamps => false
298
- if model.offroad_group_base? && batch.size > 0
299
- GroupState.for_group(model.first).create!
300
- end
301
- SendableRecordState.setup_imported(model, batch)
302
- if model.instance_methods.include?("after_offroad_upload")
303
- batch.each { |rec| rec.after_offroad_upload }
304
- end
305
- end
306
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
307
- # Need to adjust the sequences so that records inserted from this point on don't collide with existing ids
308
- cols = ActiveRecord::Base.connection.select_rows "select table_name, column_name, column_default from information_schema.columns WHERE column_default like 'nextval%'"
309
- cols.each do |table_name, column_name, column_default|
310
- if column_default =~ /nextval\('(.+)'(?:::.+)?\)/
311
- seqname = $1
312
- ActiveRecord::Base.connection.execute "SELECT setval('#{seqname}', (SELECT MAX(\"#{column_name}\") FROM \"#{table_name}\"))"
313
- end
314
- end
315
- end
316
- end
317
-
318
- def import_non_initial_model_cargo(cs, model)
319
- rrs_source = ReceivedRecordState.for_model_and_group_if_apropos(model, @group)
320
-
321
- # Update/create records
322
- cs.each_cargo_section(MirrorData::data_cargo_name_for_model(model)) do |batch|
323
- # Update foreign key associations to use local ids instead of remote ids
324
- model.reflect_on_all_associations(:belongs_to).each do |a|
325
- ReceivedRecordState.redirect_to_local_ids(batch, a.primary_key_name, a.klass, @group)
326
- end
327
-
328
- # Delete existing records in the database; that way we can just do INSERTs, don't have to worry about UPDATEs
329
- # TODO: Is this necessary? Perhaps ar-extensions can deal with a mix of new and updated records...
330
- model.delete rrs_source.all(:conditions => {:remote_record_id => batch.map(&:id)} ).map(&:local_record_id)
331
-
332
- # Update the primary keys to use local ids, then insert the records
333
- ReceivedRecordState.redirect_to_local_ids(batch, model.primary_key, model, @group)
334
- model.import batch, :validate => false, :timestamps => false
335
-
336
- if model.instance_methods.include?("after_offroad_upload")
337
- batch.each { |rec| rec.after_offroad_upload }
338
- end
339
- end
340
-
341
- # Delete records here which were destroyed there (except for group_base records, that would cause trouble)
342
- return if model == Offroad::group_base_model
343
- cs.each_cargo_section(MirrorData::deletion_cargo_name_for_model(model)) do |batch|
344
- # If there's a callback, we need to load the local records before deleting them
345
- local_recs = []
346
- if model.instance_methods.include?("after_offroad_destroy")
347
- local_recs = model.all(:conditions => {:id => batch.map(&:local_record_id)})
348
- end
349
-
350
- # Each deletion batch is made up of SendableRecordStates from the remote system
351
- dying_rrs_batch = rrs_source.all(:conditions => {:remote_record_id => batch.map(&:local_record_id)})
352
- model.delete dying_rrs_batch.map(&:local_record_id)
353
- ReceivedRecordState.delete dying_rrs_batch.map(&:id)
354
- local_recs.each { |rec| rec.after_offroad_destroy }
355
- end
356
- end
357
-
358
- def validate_imported_models(cs)
359
- Offroad::group_base_model.connection.clear_query_cache
360
- while @imported_models_to_validate.size > 0
361
- model = @imported_models_to_validate.pop
362
- rrs_source = Offroad::ReceivedRecordState.for_model_and_group_if_apropos(model, @group)
363
-
364
- cs.each_cargo_section(MirrorData::data_cargo_name_for_model(model)) do |cargo_batch|
365
- if @initial_mode
366
- local_batch = model.all(:conditions => {:id => cargo_batch.map(&:id)})
367
- else
368
- local_rrs_batch = rrs_source.all(:conditions => {:remote_record_id => cargo_batch.map(&:id)})
369
- local_batch = model.all(:conditions => {:id => local_rrs_batch.map(&:local_record_id)})
370
- end
371
- raise Offroad::DataError.new("Invalid record found in mirror data") unless local_batch.all?(&:valid?)
372
- end
373
- end
374
- end
375
- end
376
- end
1
+ module Offroad
2
+ private
3
+
4
+ class MirrorData
5
+ attr_reader :group, :mode
6
+
7
+ def initialize(group, options = {})
8
+ @group = group
9
+ @initial_mode = options.delete(:initial_mode) || false
10
+ @skip_validation = options.delete(:skip_validation) || false
11
+
12
+ raise PluginError.new("Invalid option keys") unless options.size == 0
13
+
14
+ unless Offroad::app_offline? && @initial_mode
15
+ raise PluginError.new("Need group") unless @group.is_a?(Offroad::group_base_model) && !@group.new_record?
16
+ raise DataError.new("Group must be in offline mode") unless @group.group_offline?
17
+ end
18
+
19
+ @imported_models_to_validate = []
20
+ end
21
+
22
+ def write_upwards_data(tgt = nil)
23
+ raise PluginError.new("Can only write upwards data in offline mode") unless Offroad.app_offline?
24
+ raise PluginError.new("No such thing as initial upwards data") if @initial_mode
25
+ write_data(tgt) do |cs|
26
+ add_group_specific_cargo(cs)
27
+ end
28
+ end
29
+
30
+ def write_downwards_data(tgt = nil)
31
+ raise PluginError.new("Can only write downwards data in online mode") unless Offroad.app_online?
32
+ write_data(tgt) do |cs|
33
+ add_global_cargo(cs)
34
+ if @initial_mode
35
+ add_group_specific_cargo(cs)
36
+ end
37
+ end
38
+ end
39
+
40
+ def load_upwards_data(src)
41
+ raise PluginError.new("Can only load upwards data in online mode") unless Offroad.app_online?
42
+ raise PluginError.new("No such thing as initial upwards data") if @initial_mode
43
+
44
+ read_data_from("offline", src) do |cs, mirror_info, cargo_group_state|
45
+ unless cargo_group_state.confirmed_group_data_version > @group.group_state.confirmed_group_data_version
46
+ raise OldDataError.new("File contains old up-mirror data")
47
+ end
48
+ import_group_specific_cargo(cs)
49
+ @group.group_offline = false if cargo_group_state.group_locked?
50
+ end
51
+ end
52
+
53
+ def load_downwards_data(src)
54
+ raise PluginError.new("Can only load downwards data in offline mode") unless Offroad.app_offline?
55
+
56
+ read_data_from("online", src) do |cs, mirror_info, cargo_group_state|
57
+ raise DataError.new("Unexpected initial file value") unless mirror_info.initial_file == @initial_mode
58
+
59
+ group_cargo_name = MirrorData::data_cargo_name_for_model(Offroad::group_base_model)
60
+ if mirror_info.initial_file
61
+ raise DataError.new("No group data in initial down mirror file") unless cs.has_cargo_named?(group_cargo_name)
62
+ # This is an initial mirror file, so we want it to determine the entirety of the database's new state
63
+ # However, existing data is safe if there's a mid-import error; read_data_from places us in a transaction
64
+ delete_all_existing_database_records!
65
+
66
+ import_global_cargo(cs) # Global cargo must be done first because group data might belong_to global data
67
+ import_group_specific_cargo(cs)
68
+ else
69
+ # Regular, non-initial down mirror file
70
+ unless cargo_group_state.confirmed_global_data_version > @group.group_state.confirmed_global_data_version
71
+ raise OldDataError.new("File contains old down-mirror data")
72
+ end
73
+ import_global_cargo(cs)
74
+ end
75
+
76
+ # Load information into our group state that the online app is in a better position to know about
77
+ @group = Offroad::offline_group if @initial_mode
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def self.data_cargo_name_for_model(model)
84
+ "data_#{model.name}"
85
+ end
86
+
87
+ def self.deletion_cargo_name_for_model(model)
88
+ "deletion_#{model.name}"
89
+ end
90
+
91
+ def delete_all_existing_database_records!
92
+ tables = ActiveRecord::Base.connection.tables
93
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
94
+ # Emptying sqlite_sequence resets SQLite's autoincrement counters.
95
+ # SQLite's autoincrement is nice in that it automatically picks largest ever id + 1.
96
+ # This means that after clearing sqlite_sequence and then populating database with manually-id'd rows,
97
+ # new records will be inserted with unique id's, no problem.
98
+ tables << "sqlite_sequence"
99
+ end
100
+
101
+ tables.each do |table|
102
+ next if table.start_with?("virtual_") # Used in testing # FIXME Should pick something less likely to collide with app name
103
+ next if table == "schema_migrations"
104
+ ActiveRecord::Base.connection.execute "DELETE FROM #{table}"
105
+ end
106
+
107
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
108
+ # Reset all sequences so that autoincremented ids start from 1 again
109
+ seqnames = ActiveRecord::Base.connection.select_values "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
110
+ seqnames.each do |s|
111
+ ActiveRecord::Base.connection.execute "SELECT setval('#{s}', 1, false)"
112
+ end
113
+ end
114
+ end
115
+
116
+ def write_data(tgt)
117
+ cs = nil
118
+ temp_sio = nil
119
+ case tgt
120
+ when CargoStreamer
121
+ cs = tgt
122
+ when nil
123
+ temp_sio = StringIO.new("", "w")
124
+ cs = CargoStreamer.new(temp_sio, "w")
125
+ else
126
+ cs = CargoStreamer.new(tgt, "w")
127
+ end
128
+
129
+ # TODO: Figure out if this transaction ensures we get a consistent read state
130
+ Offroad::group_base_model.connection.transaction do
131
+ Offroad::group_base_model.cache do
132
+ begin
133
+ mirror_info = MirrorInfo.new_from_group(@group, @initial_mode)
134
+ cs.write_cargo_section("mirror_info", [mirror_info], :human_readable => true)
135
+
136
+ group_state = @group.group_state
137
+ if Offroad::app_online?
138
+ # Let the offline app know what global data version it's being updated to
139
+ group_state.confirmed_global_data_version = SystemState::current_mirror_version
140
+ else
141
+ # Let the online app know what group data version the online mirror of this group is being updated to
142
+ group_state.confirmed_group_data_version = SystemState::current_mirror_version
143
+ end
144
+ cs.write_cargo_section("group_state", [group_state], :human_readable => true)
145
+
146
+ yield cs
147
+
148
+ SystemState::increment_mirror_version
149
+ rescue Offroad::CargoStreamerError
150
+ raise Offroad::DataError.new("Encountered data validation error while writing to cargo file")
151
+ end
152
+ end
153
+ end
154
+
155
+ return temp_sio.string if temp_sio
156
+ end
157
+
158
+ def read_data_from(expected_source_app_mode, src)
159
+ cs = case src
160
+ when CargoStreamer then src
161
+ when String then CargoStreamer.new(StringIO.new(src, "r"), "r")
162
+ else CargoStreamer.new(src, "r")
163
+ end
164
+
165
+ raise DataError.new("Invalid mirror file, no info section found") unless cs.has_cargo_named?("mirror_info")
166
+ mirror_info = cs.first_cargo_element("mirror_info")
167
+ raise DataError.new("Invalid info section type") unless mirror_info.is_a?(MirrorInfo)
168
+ unless mirror_info.app_mode.downcase == expected_source_app_mode.downcase
169
+ raise DataError.new "Mirror file was generated by app in wrong mode; was expecting #{expected_source_app_mode}"
170
+ end
171
+
172
+ raise DataError.new("Invalid mirror file, no group state found") unless cs.has_cargo_named?("group_state")
173
+ group_state = cs.first_cargo_element("group_state")
174
+ raise DataError.new("Invalid group state type") unless group_state.is_a?(GroupState)
175
+ group_state.readonly!
176
+
177
+ # FIXME: Is this transaction call helping at all?
178
+ Offroad::group_base_model.connection.transaction do
179
+ Offroad::group_base_model.cache do
180
+ yield cs, mirror_info, group_state
181
+ validate_imported_models(cs) unless @skip_validation
182
+
183
+ # Load information into our group state that the remote app is in a better position to know about
184
+ @group.group_state.update_from_remote_group_state!(group_state) if @group && @group.group_offline?
185
+ end
186
+ end
187
+
188
+ SystemState::increment_mirror_version if @initial_mode
189
+ end
190
+
191
+ def add_group_specific_cargo(cs)
192
+ Offroad::group_owned_models.each do |name, model|
193
+ add_model_cargo(cs, model)
194
+ end
195
+ Offroad::group_single_models.each do |name, model|
196
+ add_model_cargo(cs, model)
197
+ end
198
+ add_model_cargo(cs, Offroad::group_base_model)
199
+ end
200
+
201
+ def add_global_cargo(cs)
202
+ Offroad::global_data_models.each do |name, model|
203
+ add_model_cargo(cs, model)
204
+ end
205
+ end
206
+
207
+ def add_model_cargo(cs, model)
208
+ if @initial_mode
209
+ add_initial_model_cargo(cs, model)
210
+ else
211
+ add_non_initial_model_cargo(cs, model)
212
+ end
213
+ end
214
+
215
+ def add_initial_model_cargo(cs, model)
216
+ # Include the data for relevant records in this model
217
+ data_source = model
218
+ data_source = data_source.owned_by_offroad_group(@group) if model.offroad_group_data? && @group
219
+ data_source.find_in_batches(:batch_size => 100) do |batch|
220
+ cs.write_cargo_section(
221
+ MirrorData::data_cargo_name_for_model(model),
222
+ batch,
223
+ :skip_validation => @skip_validation
224
+ )
225
+
226
+ if model.offroad_group_data?
227
+ # In initial mode the remote app will create records with the same id's as the corresponding records here
228
+ # So we'll create RRSes indicating that we've already "received" the data we're about to send
229
+ # Later when the remote app sends new information on those records, we'll know which ones it means
230
+ rrs_source = Offroad::ReceivedRecordState.for_model_and_group_if_apropos(model, @group)
231
+ existing_rrs = rrs_source.all(:conditions => {:remote_record_id => batch.map(&:id)}).index_by(&:remote_record_id)
232
+ new_rrs = batch.reject{|r| existing_rrs.has_key?(r.id)}.map{|r| rrs_source.for_record(r).new(:remote_record_id => r.id)}
233
+ if new_rrs.size > 0
234
+ Offroad::ReceivedRecordState.import(new_rrs, :validate => false, :timestamps => false)
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ def add_non_initial_model_cargo(cs, model)
241
+ # Include the data for relevant records in this model that are newer than the remote side's known latest version
242
+ gs = @group.group_state
243
+ remote_version = nil
244
+ if model.offroad_group_data?
245
+ remote_version = gs.confirmed_group_data_version
246
+ else
247
+ remote_version = gs.confirmed_global_data_version
248
+ end
249
+ srs_source = SendableRecordState.for_model(model).with_version_greater_than(remote_version)
250
+ srs_source.for_non_deleted_records.find_in_batches(:batch_size => 100) do |srs_batch|
251
+ # TODO Might be able to optimize this to one query using a join on app model and SRS tables
252
+ record_ids = srs_batch.map { |srs| srs.local_record_id }
253
+ data_batch = model.find(:all, :conditions => {:id => record_ids})
254
+ raise PluginError.new("Invalid SRS ids") if data_batch.size != srs_batch.size
255
+ cs.write_cargo_section(
256
+ MirrorData::data_cargo_name_for_model(model),
257
+ data_batch,
258
+ :skip_validation => @skip_validation
259
+ )
260
+ end
261
+
262
+ # Also need to include information about records that have been destroyed
263
+ srs_source.for_deleted_records.find_in_batches(:batch_size => 100) do |deletion_batch|
264
+ cs.write_cargo_section(MirrorData::deletion_cargo_name_for_model(model), deletion_batch)
265
+ end
266
+ end
267
+
268
+ def import_group_specific_cargo(cs)
269
+ import_model_cargo(cs, Offroad::group_base_model)
270
+ Offroad::group_owned_models.each do |name, model|
271
+ import_model_cargo(cs, model)
272
+ end
273
+ Offroad::group_single_models.each do |name, model|
274
+ import_model_cargo(cs, model)
275
+ end
276
+ end
277
+
278
+ def import_global_cargo(cs)
279
+ Offroad::global_data_models.each do |name, model|
280
+ import_model_cargo(cs, model)
281
+ end
282
+ end
283
+
284
+ def import_model_cargo(cs, model)
285
+ @imported_models_to_validate.push model
286
+
287
+ if @initial_mode && model.offroad_group_data?
288
+ import_initial_model_cargo(cs, model)
289
+ else
290
+ import_non_initial_model_cargo(cs, model)
291
+ end
292
+ end
293
+
294
+ def import_initial_model_cargo(cs, model)
295
+ cs.each_cargo_section(MirrorData::data_cargo_name_for_model(model)) do |batch|
296
+ # Notice we are using the same primary key values as the online system, not allocating new ones
297
+ model.import batch, :validate => false, :timestamps => false
298
+ if model.offroad_group_base? && batch.size > 0
299
+ GroupState.for_group(model.first).create!
300
+ end
301
+ SendableRecordState.setup_imported(model, batch)
302
+ if model.instance_methods.include?("after_offroad_upload")
303
+ batch.each { |rec| rec.after_offroad_upload }
304
+ end
305
+ end
306
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
307
+ # Need to adjust the sequences so that records inserted from this point on don't collide with existing ids
308
+ cols = ActiveRecord::Base.connection.select_rows "select table_name, column_name, column_default from information_schema.columns WHERE column_default like 'nextval%'"
309
+ cols.each do |table_name, column_name, column_default|
310
+ if column_default =~ /nextval\('(.+)'(?:::.+)?\)/
311
+ seqname = $1
312
+ ActiveRecord::Base.connection.execute "SELECT setval('#{seqname}', (SELECT MAX(\"#{column_name}\") FROM \"#{table_name}\"))"
313
+ end
314
+ end
315
+ end
316
+ end
317
+
318
+ def import_non_initial_model_cargo(cs, model)
319
+ rrs_source = ReceivedRecordState.for_model_and_group_if_apropos(model, @group)
320
+
321
+ # Update/create records
322
+ cs.each_cargo_section(MirrorData::data_cargo_name_for_model(model)) do |batch|
323
+ # Update foreign key associations to use local ids instead of remote ids
324
+ model.reflect_on_all_associations(:belongs_to).each do |a|
325
+ ReceivedRecordState.redirect_to_local_ids(batch, a.primary_key_name, a.klass, @group)
326
+ end
327
+
328
+ # Delete existing records in the database; that way we can just do INSERTs, don't have to worry about UPDATEs
329
+ # TODO: Is this necessary? Perhaps ar-extensions can deal with a mix of new and updated records...
330
+ model.delete rrs_source.all(:conditions => {:remote_record_id => batch.map(&:id)} ).map(&:local_record_id)
331
+
332
+ # Update the primary keys to use local ids, then insert the records
333
+ ReceivedRecordState.redirect_to_local_ids(batch, model.primary_key, model, @group)
334
+ model.import batch, :validate => false, :timestamps => false
335
+
336
+ if model.instance_methods.include?("after_offroad_upload")
337
+ batch.each { |rec| rec.after_offroad_upload }
338
+ end
339
+ end
340
+
341
+ # Delete records here which were destroyed there (except for group_base records, that would cause trouble)
342
+ return if model == Offroad::group_base_model
343
+ cs.each_cargo_section(MirrorData::deletion_cargo_name_for_model(model)) do |batch|
344
+ # If there's a callback, we need to load the local records before deleting them
345
+ local_recs = []
346
+ if model.instance_methods.include?("after_offroad_destroy")
347
+ local_recs = model.all(:conditions => {:id => batch.map(&:local_record_id)})
348
+ end
349
+
350
+ # Each deletion batch is made up of SendableRecordStates from the remote system
351
+ dying_rrs_batch = rrs_source.all(:conditions => {:remote_record_id => batch.map(&:local_record_id)})
352
+ model.delete dying_rrs_batch.map(&:local_record_id)
353
+ ReceivedRecordState.delete dying_rrs_batch.map(&:id)
354
+ local_recs.each { |rec| rec.after_offroad_destroy }
355
+ end
356
+ end
357
+
358
+ def validate_imported_models(cs)
359
+ Offroad::group_base_model.connection.clear_query_cache
360
+ while @imported_models_to_validate.size > 0
361
+ model = @imported_models_to_validate.pop
362
+ rrs_source = Offroad::ReceivedRecordState.for_model_and_group_if_apropos(model, @group)
363
+
364
+ cs.each_cargo_section(MirrorData::data_cargo_name_for_model(model)) do |cargo_batch|
365
+ if @initial_mode
366
+ local_batch = model.all(:conditions => {:id => cargo_batch.map(&:id)})
367
+ else
368
+ local_rrs_batch = rrs_source.all(:conditions => {:remote_record_id => cargo_batch.map(&:id)})
369
+ local_batch = model.all(:conditions => {:id => local_rrs_batch.map(&:local_record_id)})
370
+ end
371
+ raise Offroad::DataError.new("Invalid record found in mirror data") unless local_batch.all?(&:valid?)
372
+ end
373
+ end
374
+ end
375
+ end
376
+ end