@@ -189,20 +189,13 @@ fn should_not_collect_cloud_objects_referenced_by_manifest() {
189189}
190190
191191/// Simulates a cloud provider outage that specifically affects deleting
192- /// GC'd (orphaned) objects, without disturbing anything else, by chmod-ing
193- /// the simulated cloud bucket read-only for the exact window between
194- /// compaction's manifest publish and its orphan cleanup. This mirrors
195- /// `should_handle_cloud_unavailable_during_eviction` in
196- /// `tests/hybrid_storage.rs`, which uses the same "the simulated cloud
197- /// backend is just a filesystem directory" trick for upload outages; here
198- /// we pin the outage to the GC boundary using the existing
199- /// `slice6::after_manifest_persist_before_sst_gc` failpoint so the prior
200- /// (successful) upload of the compacted output isn't itself blocked.
192+ /// GC'd (orphaned) objects, without disturbing the output upload that must
193+ /// precede manifest publication. A provider-boundary failpoint keeps this
194+ /// deterministic even when the process has permission to delete read-only
195+ /// files, as root does inside the Docker qualification image.
201196#[ test]
202- #[ cfg( all ( feature = "failpoints" , unix ) ) ]
197+ #[ cfg( feature = "failpoints" ) ]
203198fn should_handle_gc_when_cloud_delete_fails ( ) {
204- use std:: os:: unix:: fs:: PermissionsExt ;
205-
206199 let _guard = failpoint_test_lock ( )
207200 . lock ( )
208201 . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) ;
@@ -217,7 +210,7 @@ fn should_handle_gc_when_cloud_delete_fails() {
217210 . build ( )
218211 . expect ( "build simulated cloud options" ) ;
219212 let l0_batch_size = options. l0_compaction_trigger ( ) ;
220- let engine = Engine :: open ( options) . expect ( "open simulated cloud engine" ) ;
213+ let mut engine = Engine :: open ( options) . expect ( "open simulated cloud engine" ) ;
221214 let cf = engine. create_column_family ( "test" ) . expect ( "create cf" ) ;
222215
223216 // Write exactly one configured L0 batch. This isolates the delete failure
@@ -247,52 +240,23 @@ fn should_handle_gc_when_cloud_delete_fails() {
247240 "expected one configured L0 batch to be mirrored to cloud storage, got {before_objects:?}"
248241 ) ;
249242
250- // Arm a failpoint at the exact boundary between compaction's manifest
251- // publish (which already uploaded the compacted output) and its
252- // orphan-SST GC pass, and make the bucket read-only there so only the
253- // orphan delete fails.
243+ // Arm only the remote SST delete boundary. The compacted output upload
244+ // and manifest authority switch remain real simulated-cloud operations.
254245 let scenario = fail:: FailScenario :: setup ( ) ;
255- let original_permissions = std:: fs:: metadata ( & cloud_sst_dir)
256- . expect ( "stat cloud sst dir" )
257- . permissions ( ) ;
258- let outage_dir = cloud_sst_dir. clone ( ) ;
259- fail:: cfg_callback ( "slice6::after_manifest_persist_before_sst_gc" , move || {
260- std:: fs:: set_permissions ( & outage_dir, std:: fs:: Permissions :: from_mode ( 0o500 ) )
261- . expect ( "simulate cloud delete outage via read-only bucket" ) ;
262- } )
263- . expect ( "configure cloud delete outage failpoint" ) ;
246+ fail:: cfg ( "midge::cloud::inject_fail_sst_delete" , "return" )
247+ . expect ( "configure cloud delete outage failpoint" ) ;
264248
265249 // Act: compaction should orphan the selected input SSTs and try (and fail)
266- // to delete them from the now-read-only cloud bucket .
250+ // to delete them from cloud storage .
267251 let compact_result = engine. compact_all ( ) ;
268252
269- // Give the async cloud-delete worker time to attempt (and fail) the
270- // delete before we inspect the bucket.
271- thread:: sleep ( Duration :: from_millis ( 300 ) ) ;
272-
273- fail:: remove ( "slice6::after_manifest_persist_before_sst_gc" ) ;
274- std:: fs:: set_permissions ( & cloud_sst_dir, original_permissions)
275- . expect ( "restore cloud sst permissions" ) ;
276- scenario. teardown ( ) ;
277-
278253 // Assert: compaction tolerates the delete failure rather than
279254 // propagating it as an error.
280255 assert ! (
281256 compact_result. is_ok( ) ,
282257 "compact_all should tolerate a cloud delete failure: {compact_result:?}"
283258 ) ;
284259
285- // Assert: the orphaned objects are still present in cloud storage
286- // because their delete genuinely failed and was retained for retry,
287- // not silently skipped or corrupted.
288- let after_objects = sst_object_names ( & cloud_sst_dir) ;
289- let retained: Vec < _ > = before_objects. intersection ( & after_objects) . collect ( ) ;
290- assert ! (
291- !retained. is_empty( ) ,
292- "expected the orphaned cloud objects whose delete failed to remain \
293- in cloud storage for retry, got {after_objects:?}"
294- ) ;
295-
296260 // Assert: engine remains fully functional; no data was lost.
297261 let tx = engine
298262 . begin_tx ( cf. id ( ) , TransactionMode :: ReadOnly )
@@ -304,6 +268,27 @@ fn should_handle_gc_when_cloud_delete_fails() {
304268 "data lost after cloud delete failure"
305269 ) ;
306270 }
271+ drop ( tx) ;
272+
273+ // Shutdown joins every cloud-delete worker while the outage remains
274+ // armed, so the filesystem observation cannot race an unattempted delete.
275+ engine
276+ . shutdown ( Duration :: from_secs ( 10 ) )
277+ . expect ( "shutdown after failed cloud delete" ) ;
278+ let after_objects = sst_object_names ( & cloud_sst_dir) ;
279+ let retained: Vec < _ > = before_objects. intersection ( & after_objects) . collect ( ) ;
280+
281+ fail:: remove ( "midge::cloud::inject_fail_sst_delete" ) ;
282+ scenario. teardown ( ) ;
283+
284+ // Assert: the orphaned objects are still present in cloud storage
285+ // because their delete genuinely failed and was retained for retry,
286+ // not silently skipped or corrupted.
287+ assert ! (
288+ !retained. is_empty( ) ,
289+ "expected the orphaned cloud objects whose delete failed to remain \
290+ in cloud storage for retry, got {after_objects:?}"
291+ ) ;
307292
308293 eprintln ! ( "✓ Engine gracefully handled cloud delete failure" ) ;
309294}
0 commit comments