summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2026-05-11 15:56:08 -0700
committergerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2026-05-12 10:41:47 -0700
commitd5037230e94e50c30698f9dd8d9404e293e23c7e (patch)
tree3e91817d0984bdef99f2fdc4f982675de328f5c0
parent7cc99b24c1869d54168708a2df6e6f936c36f980 (diff)
downloadgit-repo-d5037230e94e50c30698f9dd8d9404e293e23c7e.tar.gz
sync: Re-raise KeyboardInterrupt in main process
When running sync -j1, worker functions run directly in the main process. Swallowing KeyboardInterrupt causes the loop to continue to the next project instead of aborting. Re-raise KeyboardInterrupt if running in the MainProcess, while maintaining the suppression of stack traces in worker processes. Bug: 468170157 Change-Id: I156d66bc209a265f7fa25eea0eb88737d1b51a34 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/581342 Commit-Queue: Gavin Mak <gavinmak@google.com> Reviewed-by: Mike Frysinger <vapier@google.com> Tested-by: Gavin Mak <gavinmak@google.com>
-rw-r--r--command.py5
-rw-r--r--subcmds/sync.py8
-rw-r--r--tests/test_subcmds_sync.py50
3 files changed, 63 insertions, 0 deletions
diff --git a/command.py b/command.py
index e74a94fa3..ee1f68c14 100644
--- a/command.py
+++ b/command.py
@@ -101,6 +101,11 @@ class Command:
101 def WantPager(self, _opt): 101 def WantPager(self, _opt):
102 return False 102 return False
103 103
104 @staticmethod
105 def is_multiprocessing_active() -> bool:
106 """Whether the current process is a worker in a pool."""
107 return multiprocessing.current_process().name != "MainProcess"
108
104 def ReadEnvironmentOptions(self, opts): 109 def ReadEnvironmentOptions(self, opts):
105 """Set options from environment variables.""" 110 """Set options from environment variables."""
106 111
diff --git a/subcmds/sync.py b/subcmds/sync.py
index b1c30867e..2517694e7 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -841,6 +841,8 @@ later is required to fix a server side protocol bug.
841 ) 841 )
842 except KeyboardInterrupt: 842 except KeyboardInterrupt:
843 logger.error("Keyboard interrupt while processing %s", project.name) 843 logger.error("Keyboard interrupt while processing %s", project.name)
844 if not cls.is_multiprocessing_active():
845 raise
844 except GitError as e: 846 except GitError as e:
845 logger.error("error.GitError: Cannot fetch %s", e) 847 logger.error("error.GitError: Cannot fetch %s", e)
846 errors.append(e) 848 errors.append(e)
@@ -1104,6 +1106,8 @@ later is required to fix a server side protocol bug.
1104 errors.extend(syncbuf.errors) 1106 errors.extend(syncbuf.errors)
1105 except KeyboardInterrupt: 1107 except KeyboardInterrupt:
1106 logger.error("Keyboard interrupt while processing %s", project.name) 1108 logger.error("Keyboard interrupt while processing %s", project.name)
1109 if not cls.is_multiprocessing_active():
1110 raise
1107 except GitError as e: 1111 except GitError as e:
1108 logger.error( 1112 logger.error(
1109 "error.GitError: Cannot checkout %s: %s", project.name, e 1113 "error.GitError: Cannot checkout %s: %s", project.name, e
@@ -2410,6 +2414,8 @@ later is required to fix a server side protocol bug.
2410 logger.error( 2414 logger.error(
2411 "Keyboard interrupt while processing %s", project.name 2415 "Keyboard interrupt while processing %s", project.name
2412 ) 2416 )
2417 if not cls.is_multiprocessing_active():
2418 raise
2413 except GitError as e: 2419 except GitError as e:
2414 fetch_errors.append(e) 2420 fetch_errors.append(e)
2415 logger.error("error.GitError: Cannot fetch %s", e) 2421 logger.error("error.GitError: Cannot fetch %s", e)
@@ -2460,6 +2466,8 @@ later is required to fix a server side protocol bug.
2460 logger.error( 2466 logger.error(
2461 "Keyboard interrupt while processing %s", project.name 2467 "Keyboard interrupt while processing %s", project.name
2462 ) 2468 )
2469 if not cls.is_multiprocessing_active():
2470 raise
2463 except GitError as e: 2471 except GitError as e:
2464 checkout_errors.append(e) 2472 checkout_errors.append(e)
2465 logger.error( 2473 logger.error(
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index 0a46bbdf3..669027c98 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -477,6 +477,56 @@ class GetPreciousObjectsState(unittest.TestCase):
477 ) 477 )
478 478
479 479
480class KeyboardInterruptTest(unittest.TestCase):
481 """Tests for KeyboardInterrupt handling in Sync operations."""
482
483 def setUp(self):
484 self.project = mock.MagicMock(name="project")
485 self.project.name = "project"
486 self.project.relpath = "proj"
487 self.project.manifest.IsArchive = False
488 self.opt = mock.Mock()
489 self.opt.quiet = True
490 self.opt.verbose = False
491 self.opt.tags = False
492
493 self.sync_dict = {}
494
495 self.get_parallel_context_mock = {
496 "projects": [self.project],
497 "sync_dict": self.sync_dict,
498 "ssh_proxy": None,
499 }
500
501 @mock.patch("subcmds.sync.Sync.is_multiprocessing_active")
502 def test_fetch_one_keyboard_interrupt_main_process(self, mock_is_active):
503 """Test that _FetchOne re-raises KeyboardInterrupt if not worker."""
504 mock_is_active.return_value = False
505 self.project.Sync_NetworkHalf.side_effect = KeyboardInterrupt()
506
507 with mock.patch.object(
508 sync.Sync,
509 "get_parallel_context",
510 return_value=self.get_parallel_context_mock,
511 ):
512 with self.assertRaises(KeyboardInterrupt):
513 sync.Sync._FetchOne(self.opt, 0)
514
515 @mock.patch("subcmds.sync.Sync.is_multiprocessing_active")
516 def test_fetch_one_keyboard_interrupt_worker_process(self, mock_is_active):
517 """Test that _FetchOne suppresses KeyboardInterrupt in workers."""
518 mock_is_active.return_value = True
519 self.project.Sync_NetworkHalf.side_effect = KeyboardInterrupt()
520
521 with mock.patch.object(
522 sync.Sync,
523 "get_parallel_context",
524 return_value=self.get_parallel_context_mock,
525 ):
526 result = sync.Sync._FetchOne(self.opt, 0)
527 self.assertFalse(result.success)
528
529
480class CheckForBloatedProjects(unittest.TestCase): 530class CheckForBloatedProjects(unittest.TestCase):
481 """Tests for Sync._CheckForBloatedProjects.""" 531 """Tests for Sync._CheckForBloatedProjects."""
482 532