)]}'
{"/COMMIT_MSG":[{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"45bfc40e0c65b56757dfccfe26dc2c43b07fa3c9","unresolved":false,"context_lines":[{"line_number":13,"context_line":"Deferring cleanup may leave an empty suffix directory until the batch"},{"line_number":14,"context_line":"finishes, rather than removing it as soon as its final hash is deleted."},{"line_number":15,"context_line":"Object data is still removed immediately. The delay avoids repeated path"},{"line_number":16,"context_line":"lookups and ENOTEMPTY results. The collected set is bounded by the 4096"},{"line_number":17,"context_line":"possible suffixes."},{"line_number":18,"context_line":""},{"line_number":19,"context_line":"Keep cleanup failures isolated to one suffix so later empty suffixes can"},{"line_number":20,"context_line":"still be removed. Log unexpected cleanup and quarantine failures with"},{"line_number":21,"context_line":"exception context without aborting the remaining suffix cleanup."}],"source_content_type":"text/x-gerrit-commit-message","patch_set":3,"id":"e1a26ca0_4fb4c4f5","line":18,"range":{"start_line":16,"start_character":32,"end_line":18,"end_character":0},"updated":"2026-09-16 17:03:21.000000000","message":"This sentence reads to me that the bound is enforced in the code.","commit_id":"7f2a0529b41781abb608536a8122135bb404fec6"}],"/PATCHSET_LEVEL":[{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"45bfc40e0c65b56757dfccfe26dc2c43b07fa3c9","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":3,"id":"5da493db_52b98fa9","updated":"2026-09-16 17:03:21.000000000","message":"LGTM!","commit_id":"7f2a0529b41781abb608536a8122135bb404fec6"}],"test/unit/obj/test_replicator.py":[{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"0116d71e01cf5703d354036abddc46759f7f74dd","unresolved":false,"context_lines":[{"line_number":1837,"context_line":"    def _run_delete_partition_ssync_suffix_cleanup("},{"line_number":1838,"context_line":"            self, rmdir_err, quarantine_error\u003dNone):"},{"line_number":1839,"context_line":"        candidates \u003d {}"},{"line_number":1840,"context_line":"        for obj in (\u0027o1\u0027, \u0027o2\u0027):"},{"line_number":1841,"context_line":"            df \u003d self.df_mgr.get_diskfile("},{"line_number":1842,"context_line":"                \u0027sda\u0027, \u00271\u0027, \u0027a\u0027, \u0027c\u0027, obj, policy\u003dPOLICIES.legacy)"},{"line_number":1843,"context_line":"            mkdirs(df._datadir)"}],"source_content_type":"text/x-python","patch_set":3,"id":"b5674e3f_8ed986c4","line":1840,"updated":"2026-09-16 16:51:06.000000000","message":"Looks good to me on PS3. I checked that handoff selection and partition locking are unchanged, that unselected/new objects keep their suffix nonempty, and that cleanup/quarantine failures stay isolated to one suffix with exception context.\n\nLocal validation: Python 3.12.3, TMPDIR on a filesystem with large-xattr support; all 80 replicator tests passed, three targeted diskfile quarantine tests passed, six additional edge-case tests passed, and the test below passed. Flake8 on both changed files and git diff --check passed. I did not run cluster probes or the full CI matrix.\n\nNonblocking test suggestion: these two objects have different suffixes, so the tests do not establish the batching optimization. I kept the new cleanup helper/error handling but called it after each object\u0027s rmtree; all 80 replicator tests still passed. The small test below passes on PS3 and fails with that mutation because the shared suffix gets two rmdir attempts. It also verifies that every selected hash is gone before suffix cleanup starts. This can be a follow-up since the current implementation is correct and the demonstrated regression is redundant cleanup work.\n\n```diff\ndiff --git a/test/unit/obj/test_replicator.py b/test/unit/obj/test_replicator.py\nindex f8c1a09ba..403c572ed 100644\n--- a/test/unit/obj/test_replicator.py\n+++ b/test/unit/obj/test_replicator.py\n@@ -1715,6 +1715,36 @@ class TestObjectReplicator(BaseUnitTestCase):\n \n             del self.call_nums\n \n+    def test_delete_handoff_objs_cleans_shared_suffix_once(self):\n+        hashes \u003d [\u00270\u0027 * 29 + \u0027abc\u0027, \u00271\u0027 * 29 + \u0027abc\u0027, \u00272\u0027 * 29 + \u0027def\u0027]\n+        paths \u003d [storage_directory(self.objects, \u00271\u0027, value)\n+                 for value in hashes]\n+        for path in paths:\n+            mkdirs(path)\n+            with open(os.path.join(path, self.ts().internal + \u0027.data\u0027),\n+                      \u0027wb\u0027) as stream:\n+                stream.write(b\u00270\u0027)\n+        suffixes \u003d {os.path.dirname(path) for path in paths}\n+        attempts \u003d collections.Counter()\n+        remaining_at_cleanup \u003d []\n+        real_rmdir \u003d os.rmdir\n+\n+        def observe_rmdir(path, *args, **kwargs):\n+            if path in suffixes:\n+                attempts[path] +\u003d 1\n+                remaining_at_cleanup.append(\n+                    [p for p in paths if os.path.exists(p)])\n+            return real_rmdir(path, *args, **kwargs)\n+\n+        job \u003d {\u0027obj_path\u0027: self.objects, \u0027partition\u0027: \u00271\u0027}\n+        with mock.patch(\u0027os.rmdir\u0027, side_effect\u003dobserve_rmdir):\n+            self.replicator.delete_handoff_objs(job, hashes)\n+\n+        self.assertEqual(attempts, {suffix: 1 for suffix in suffixes})\n+        self.assertEqual(remaining_at_cleanup, [[], []])\n+        self.assertTrue(all(not os.path.exists(s) for s in suffixes))\n+        self.assertEqual(self.logger.get_lines_for_level(\u0027error\u0027), [])\n+\n     def test_delete_partition_ssync_cleanup_enoent_continues(self):\n         rmdir_err \u003d OSError(ENOENT, os.strerror(ENOENT))\n         failed_suffix, other_suffix \u003d \\\n```","commit_id":"7f2a0529b41781abb608536a8122135bb404fec6"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"45bfc40e0c65b56757dfccfe26dc2c43b07fa3c9","unresolved":false,"context_lines":[{"line_number":1837,"context_line":"    def _run_delete_partition_ssync_suffix_cleanup("},{"line_number":1838,"context_line":"            self, rmdir_err, quarantine_error\u003dNone):"},{"line_number":1839,"context_line":"        candidates \u003d {}"},{"line_number":1840,"context_line":"        for obj in (\u0027o1\u0027, \u0027o2\u0027):"},{"line_number":1841,"context_line":"            df \u003d self.df_mgr.get_diskfile("},{"line_number":1842,"context_line":"                \u0027sda\u0027, \u00271\u0027, \u0027a\u0027, \u0027c\u0027, obj, policy\u003dPOLICIES.legacy)"},{"line_number":1843,"context_line":"            mkdirs(df._datadir)"}],"source_content_type":"text/x-python","patch_set":3,"id":"c2c2c866_033d50d8","line":1840,"in_reply_to":"b5674e3f_8ed986c4","updated":"2026-09-16 17:03:21.000000000","message":"Rogue comment from the bots. Please ignore.","commit_id":"7f2a0529b41781abb608536a8122135bb404fec6"}]}
