@@ -128,6 +128,53 @@ def test_archive_deleted_rows_with_undeleted_residue(self):
128128 # Verify that the pci_devices record has not been dropped
129129 self .assertNotIn ('pci_devices' , results )
130130
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+
131178 def _get_table_counts (self ):
132179 engine = sqlalchemy_api .get_engine ()
133180 conn = engine .connect ()
0 commit comments