@@ -128,6 +128,53 @@ def test_archive_deleted_rows_with_undeleted_residue(self):
128
128
# Verify that the pci_devices record has not been dropped
129
129
self .assertNotIn ('pci_devices' , results )
130
130
131
+ def test_archive_deleted_rows_incomplete (self ):
132
+ """This tests a scenario where archive_deleted_rows is run with
133
+ --max_rows and does not run to completion.
134
+
135
+ That is, the archive is stopped before all archivable records have been
136
+ archived. Specifically, the problematic state is when a single instance
137
+ becomes partially archived (example: 'instance_extra' record for one
138
+ instance has been archived while its 'instances' record remains). Any
139
+ access of the instance (example: listing deleted instances) that
140
+ triggers the retrieval of a dependent record that has been archived
141
+ away, results in undefined behavior that may raise an error.
142
+
143
+ We will force the system into a state where a single deleted instance
144
+ is partially archived. We want to verify that we can, for example,
145
+ successfully do a GET /servers/detail at any point between partial
146
+ archive_deleted_rows runs without errors.
147
+ """
148
+ # Boots a server, deletes it, and then tries to archive it.
149
+ server = self ._create_server ()
150
+ server_id = server ['id' ]
151
+ # Assert that there are instance_actions. instance_actions are
152
+ # interesting since we don't soft delete them but they have a foreign
153
+ # key back to the instances table.
154
+ actions = self .api .get_instance_actions (server_id )
155
+ self .assertTrue (len (actions ),
156
+ 'No instance actions for server: %s' % server_id )
157
+ self ._delete_server (server )
158
+ # Archive deleted records iteratively, 1 row at a time, and try to do a
159
+ # GET /servers/detail between each run. All should succeed.
160
+ exceptions = []
161
+ while True :
162
+ _ , _ , archived = db .archive_deleted_rows (max_rows = 1 )
163
+ try :
164
+ # Need to use the admin API to list deleted servers.
165
+ self .admin_api .get_servers (search_opts = {'deleted' : True })
166
+ except Exception as ex :
167
+ exceptions .append (ex )
168
+ if archived == 0 :
169
+ break
170
+ # FIXME(melwitt): OrphanedObjectError is raised because of the bug.
171
+ self .assertTrue (exceptions )
172
+ for ex in exceptions :
173
+ self .assertEqual (500 , ex .response .status_code )
174
+ self .assertIn ('OrphanedObjectError' , str (ex ))
175
+ # FIXME(melwitt): Uncomment when the bug is fixed.
176
+ # self.assertFalse(exceptions)
177
+
131
178
def _get_table_counts (self ):
132
179
engine = sqlalchemy_api .get_engine ()
133
180
conn = engine .connect ()
0 commit comments