-
Notifications
You must be signed in to change notification settings - Fork 1
β‘ Bolt: Parallelize existing rules fetch with folder deletion #410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b1e839e
d363bbb
966ee5f
0af1b69
32ffa58
2fc4d54
a230dab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2267,16 +2267,25 @@ def _fetch_if_valid(url: str): | |
| if existing_folders is None: | ||
| return False | ||
|
|
||
| if not no_delete: | ||
| deletion_occurred = False | ||
| # Identify folders to delete and folders to keep (scan) | ||
| folders_to_delete = [] | ||
| folders_to_scan = existing_folders.copy() | ||
|
|
||
| # Identify folders to delete | ||
| folders_to_delete = [] | ||
| if not no_delete: | ||
| for folder_data in folder_data_list: | ||
| name = folder_data["group"]["group"].strip() | ||
| if name in existing_folders: | ||
| folders_to_delete.append((name, existing_folders[name])) | ||
| if name in folders_to_scan: | ||
| del folders_to_scan[name] | ||
|
|
||
| # Start fetching rules from kept folders in background (parallel to deletions) | ||
| existing_rules_future = shared_executor.submit( | ||
| get_all_existing_rules, client, profile_id, folders_to_scan | ||
| ) | ||
|
Comment on lines
+2283
to
+2285
|
||
|
|
||
| if not no_delete: | ||
| deletion_occurred = False | ||
| if folders_to_delete: | ||
| # Parallel delete to speed up the "clean slate" phase | ||
| # Use shared_executor (3 workers) | ||
|
|
@@ -2309,10 +2318,13 @@ def _fetch_if_valid(url: str): | |
| ) | ||
| countdown_timer(60, "Waiting for deletions to propagate") | ||
|
|
||
| # Optimization: Pass the updated existing_folders to avoid redundant API call | ||
| existing_rules = get_all_existing_rules( | ||
| client, profile_id, known_folders=existing_folders | ||
| ) | ||
| # Retrieve result from background task | ||
| # If deletion occurred, we effectively used the wait time to fetch rules! | ||
| try: | ||
| existing_rules = existing_rules_future.result() | ||
| except Exception as e: | ||
| log.error(f"Failed to fetch existing rules in background: {sanitize_for_log(e)}") | ||
| existing_rules = set() | ||
|
|
||
| with concurrent.futures.ThreadPoolExecutor( | ||
| max_workers=max_workers | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
|
|
||
Check warningCode scanning / Pylint (reported by Codacy) Missing module docstring Warning test
Missing module docstring
Check warningCode scanning / Pylintpython3 (reported by Codacy) Missing module docstring Warning test
Missing module docstring
|
||
| import unittest | ||
| from unittest.mock import MagicMock, patch | ||
| import time | ||
| import concurrent.futures | ||
Check warningCode scanning / Prospector (reported by Codacy) Unused import concurrent.futures (unused-import) Warning test
Unused import concurrent.futures (unused-import)
Check noticeCode scanning / Pylint (reported by Codacy) Unused import concurrent.futures Note test
Unused import concurrent.futures
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused import concurrent.futures Note test
Unused import concurrent.futures
|
||
| import sys | ||
| import os | ||
|
|
||
| # Add root to path | ||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| import main | ||
Check warningCode scanning / Prospector (reported by Codacy) Import "import main" should be placed at the top of the module (wrong-import-position) Warning test
Import "import main" should be placed at the top of the module (wrong-import-position)
Check warningCode scanning / Pylint (reported by Codacy) Import "import main" should be placed at the top of the module Warning test
Import "import main" should be placed at the top of the module
Check warningCode scanning / Pylintpython3 (reported by Codacy) Import "import main" should be placed at the top of the module Warning test
Import "import main" should be placed at the top of the module
Check warningCode scanning / Prospector (reported by Codacy) Cannot import 'main' due to syntax error 'invalid syntax (, line 1141)' (syntax-error) Warning test
Cannot import 'main' due to syntax error 'invalid syntax (, line 1141)' (syntax-error)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Cannot import 'main' due to syntax error 'invalid syntax (, line 1141)' Warning test
Cannot import 'main' due to syntax error 'invalid syntax (, line 1141)'
|
||
|
|
||
| class TestParallelFetch(unittest.TestCase): | ||
Check warningCode scanning / Pylint (reported by Codacy) Missing class docstring Warning test
Missing class docstring
Check warningCode scanning / Pylintpython3 (reported by Codacy) Missing class docstring Warning test
Missing class docstring
|
||
| def setUp(self): | ||
| self.main = main | ||
| self.client = MagicMock() | ||
| self.profile_id = "test_profile" | ||
| self.folder_urls = ["https://example.com/folder.json"] | ||
|
|
||
| # Mock fetch_folder_data to return dummy data | ||
| self.folder_data = { | ||
| "group": {"group": "test_folder", "action": {"do": 0, "status": 1}}, | ||
| "rules": [{"PK": "rule1"}] | ||
| } | ||
|
|
||
| @patch("main.get_all_existing_rules") | ||
| @patch("main.delete_folder") | ||
| @patch("main.verify_access_and_get_folders") | ||
| @patch("main.fetch_folder_data") | ||
| @patch("main.validate_folder_url") | ||
| @patch("main.countdown_timer") | ||
| def test_parallel_execution(self, mock_timer, mock_validate, mock_fetch, mock_verify, mock_delete, mock_get_rules): | ||
Check warningCode scanning / Prospector (reported by Codacy) Too many arguments (7/5) (too-many-arguments) Warning test
Too many arguments (7/5) (too-many-arguments)
Check warningCode scanning / Prospector (reported by Codacy) Unused argument 'mock_timer' (unused-argument) Warning test
Unused argument 'mock_timer' (unused-argument)
Check warningCode scanning / Pylint (reported by Codacy) Too many arguments (7/5) Warning test
Too many arguments (7/5)
Check warningCode scanning / Pylint (reported by Codacy) Line too long (119/100) Warning test
Line too long (119/100)
Check warningCode scanning / Pylint (reported by Codacy) Too many local variables (16/15) Warning test
Too many local variables (16/15)
Check noticeCode scanning / Pylint (reported by Codacy) Unused argument 'mock_timer' Note test
Unused argument 'mock_timer'
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (119/100) Warning test
Line too long (119/100)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Too many arguments (7/5) Warning test
Too many arguments (7/5)
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused argument 'mock_timer' Note test
Unused argument 'mock_timer'
|
||
| """ | ||
| Verify that get_all_existing_rules runs in parallel with delete_folder. | ||
abhimehro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| # Setup mocks | ||
| mock_validate.return_value = True | ||
| mock_fetch.return_value = self.folder_data | ||
|
|
||
| # existing folders: test_folder (to be deleted), keep_folder (to be kept) | ||
| mock_verify.return_value = { | ||
| "test_folder": "id_1", | ||
| "keep_folder": "id_2" | ||
| } | ||
|
|
||
| # Latency simulation | ||
| def slow_get_rules(*args, **kwargs): | ||
Check warningCode scanning / Prospector (reported by Codacy) Unused argument 'args' (unused-argument) Warning test
Unused argument 'args' (unused-argument)
Check warningCode scanning / Prospector (reported by Codacy) Unused argument 'kwargs' (unused-argument) Warning test
Unused argument 'kwargs' (unused-argument)
Check warningCode scanning / Pylint (reported by Codacy) Missing function docstring Warning test
Missing function docstring
Check noticeCode scanning / Pylint (reported by Codacy) Unused argument 'kwargs' Note test
Unused argument 'kwargs'
Check noticeCode scanning / Pylint (reported by Codacy) Unused argument 'args' Note test
Unused argument 'args'
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused argument 'args' Note test
Unused argument 'args'
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused argument 'kwargs' Note test
Unused argument 'kwargs'
|
||
| time.sleep(1.0) | ||
| return {"rule_from_keep"} | ||
|
|
||
| def slow_delete(*args, **kwargs): | ||
Check warningCode scanning / Prospector (reported by Codacy) Unused argument 'kwargs' (unused-argument) Warning test
Unused argument 'kwargs' (unused-argument)
Check warningCode scanning / Prospector (reported by Codacy) Unused argument 'args' (unused-argument) Warning test
Unused argument 'args' (unused-argument)
Check warningCode scanning / Pylint (reported by Codacy) Missing function docstring Warning test
Missing function docstring
Check noticeCode scanning / Pylint (reported by Codacy) Unused argument 'args' Note test
Unused argument 'args'
Check noticeCode scanning / Pylint (reported by Codacy) Unused argument 'kwargs' Note test
Unused argument 'kwargs'
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused argument 'kwargs' Note test
Unused argument 'kwargs'
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Unused argument 'args' Note test
Unused argument 'args'
|
||
| time.sleep(1.0) | ||
| return True | ||
|
|
||
| mock_get_rules.side_effect = slow_get_rules | ||
| mock_delete.side_effect = slow_delete | ||
|
|
||
| # Mock create_folder and push_rules to avoid errors | ||
| with patch("main.create_folder") as mock_create, \ | ||
| patch("main.push_rules") as mock_push: | ||
|
|
||
| mock_create.return_value = "new_id" | ||
| mock_push.return_value = True | ||
|
|
||
| start = time.perf_counter() | ||
| self.main.sync_profile(self.profile_id, self.folder_urls, no_delete=False) | ||
| elapsed = time.perf_counter() - start | ||
|
|
||
| # Assertions | ||
| self.assertTrue(mock_delete.called, "delete_folder should be called") | ||
| self.assertTrue(mock_get_rules.called, "get_all_existing_rules should be called") | ||
|
|
||
| # Verify get_all_existing_rules was called with ONLY keep_folder | ||
| call_args = mock_get_rules.call_args | ||
| # args: client, profile_id, known_folders | ||
| known_folders = call_args[0][2] if len(call_args[0]) > 2 else call_args[1]['known_folders'] | ||
|
|
||
| self.assertIn("keep_folder", known_folders) | ||
| self.assertNotIn("test_folder", known_folders, "Should not fetch rules from deleted folder") | ||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (104/100) Warning test
Line too long (104/100)
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (104/100) Warning test
Line too long (104/100)
|
||
|
|
||
| # Timing check | ||
| # If serial: 1s (delete) + 1s (fetch) + overhead = ~2s | ||
| # If parallel: max(1s, 1s) + overhead = ~1s | ||
| # Note: sync_profile also has overhead (fetch_folder_data etc), but that's mocked to be fast. | ||
| # We allow some buffer, but it should be significantly less than 2s | ||
| print(f"Elapsed time: {elapsed:.2f}s") | ||
|
|
||
| # In the CURRENT implementation (serial), this test should FAIL (take > 2s) | ||
| # Once implemented, it should PASS (take < 1.5s) | ||
|
|
||
| # Verify optimization effectiveness (should be close to max(1.0, 1.0) = 1.0s, allow 1.5s buffer) | ||
| self.assertLess(elapsed, 1.5, "Parallel execution took too long") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for
folders_to_deleteandfolders_to_scanis duplicated inside and outside theif not no_delete:block. This can lead to confusion and potential bugs ifno_deleteis true, asfolders_to_deletewould be empty, butfolders_to_scanwould still be a copy ofexisting_foldersand not reflect what's actually being scanned. Thefolders_to_scanshould only be modified ifno_deleteis false.