Skip to content
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

add FlareCleanupTask #947

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

add FlareCleanupTask #947

wants to merge 2 commits into from

Conversation

nora-codecov
Copy link
Contributor

New strategy for managing flare storage

depends on codecov/shared#450

When a pull is no longer OPEN, leave the flare in order to reduce locks and wait time.
During low traffic hours (4am UTC?), run this task to clear it out of our database and Archive storage.
So we won't pay for maintaining the flare field but also won't slow down other pull actions by checking or clearing the field 🎉

@nora-codecov nora-codecov requested a review from Swatinem December 6, 2024 07:36
Copy link

github-actions bot commented Dec 6, 2024

This PR includes changes to shared. Please review them here: codecov/shared@45252f7...9f5c4fd

Copy link

codecov bot commented Dec 6, 2024

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1769 1 1768 1
View the top 1 failed tests by shortest run time
tasks/tests/unit/test_flare_cleanup.py::TestFlareCleanupTask::test_successful_run
Stack Traces | 0.309s run time
self = <worker.tasks.tests.unit.test_flare_cleanup.TestFlareCleanupTask object at 0x7f4e421ca210>
transactional_db = None
mocker = <pytest_mock.plugin.MockFixture object at 0x7f4e3be52510>

    def test_successful_run(self, transactional_db, mocker):
        mock_logs = mocker.patch("logging.Logger.info")
        mock_archive_service = mocker.patch(
            "shared.django_apps.utils.model_utils.ArchiveService"
        )
        archive_value_for_flare = {"some": "data"}
        mock_archive_service.return_value.read_file.return_value = json.dumps(
            archive_value_for_flare
        )
        mock_path = ".../to/written/object"
        mock_archive_service.return_value.write_json_data_to_storage.return_value = (
            mock_path
        )
        mock_archive_service_in_task = mocker.patch(
            "tasks.flare_cleanup.ArchiveService"
        )
        mock_archive_service_in_task.return_value.delete_file.return_value = None
    
        local_value_for_flare = {"test": "test"}
        open_pull_with_local_flare = PullFactory(
            state=PullStates.OPEN.value,
            _flare=local_value_for_flare,
            repository=RepositoryFactory(),
        )
>       assert open_pull_with_local_flare.flare == local_value_for_flare
E       AssertionError: assert {} == {'test': 'test'}
E         
E         Right contains 1 more item:
E         {'test': 'test'}
E         Use -v to get more diff

.../tests/unit/test_flare_cleanup.py:42: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

@codecov-notifications
Copy link

codecov-notifications bot commented Dec 6, 2024

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1769 1 1768 1
View the top 1 failed tests by shortest run time
tasks/tests/unit/test_flare_cleanup.py::TestFlareCleanupTask::test_successful_run
Stack Traces | 0.309s run time
self = <worker.tasks.tests.unit.test_flare_cleanup.TestFlareCleanupTask object at 0x7f4e421ca210>
transactional_db = None
mocker = <pytest_mock.plugin.MockFixture object at 0x7f4e3be52510>

    def test_successful_run(self, transactional_db, mocker):
        mock_logs = mocker.patch("logging.Logger.info")
        mock_archive_service = mocker.patch(
            "shared.django_apps.utils.model_utils.ArchiveService"
        )
        archive_value_for_flare = {"some": "data"}
        mock_archive_service.return_value.read_file.return_value = json.dumps(
            archive_value_for_flare
        )
        mock_path = ".../to/written/object"
        mock_archive_service.return_value.write_json_data_to_storage.return_value = (
            mock_path
        )
        mock_archive_service_in_task = mocker.patch(
            "tasks.flare_cleanup.ArchiveService"
        )
        mock_archive_service_in_task.return_value.delete_file.return_value = None
    
        local_value_for_flare = {"test": "test"}
        open_pull_with_local_flare = PullFactory(
            state=PullStates.OPEN.value,
            _flare=local_value_for_flare,
            repository=RepositoryFactory(),
        )
>       assert open_pull_with_local_flare.flare == local_value_for_flare
E       AssertionError: assert {} == {'test': 'test'}
E         
E         Right contains 1 more item:
E         {'test': 'test'}
E         Use -v to get more diff

.../tests/unit/test_flare_cleanup.py:42: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Copy link

github-actions bot commented Dec 6, 2024

✅ All tests successful. No failed tests were found.

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

Copy link
Contributor

@Swatinem Swatinem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of suggestions:

  • We should definitely limit / batch the query. This could yield millions of entries on the first run for all we know 🤷🏻‍♂️
  • Using the mock_storage fixture, you could assert the actual side effect of files being removed from the storage backend, instead of manually mocking very specific ArchiveService methods

Otherwise this looks good, and the following are just "personal preference" suggestions:

  • IMO, we should move core business logic out of the celery framework code. In this case this would mean having some free functions instead of all the logic being in FlareCleanupTask.run_cron_task. This means you don’t even need the db_session argument, as the code does not use sqlalchemy models, and it should make testing less awkward.
  • I’m personally not a fan of asserting log calls, but rather just the side effects we are aiming for: files being removed from storage, fields being cleared from the DB.

Comment on lines 49 to 50
# single query, loads all pulls and repos in qset into memory, deletes file in Archive 1 by 1
for pull in non_open_pulls_with_flare_in_archive:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular, the "load all pulls … into memory" part can be potentially dangerous as it might overwhelm the task.

This should use a limit query and work in batches.

Comment on lines 51 to 52
archive_service = ArchiveService(repository=pull.repository)
archive_service.delete_file(pull._flare_storage_path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it might be worth grouping these by repository(.id) on the python side, to deduplicate creation of per-repo ArchiveService. Plus, I believe there is a delete_files method to do batch deletions as well. Though I believe that depending on the storage backend, that will still delete each file individually.

_flare_storage_path__isnull=False
).select_related("repository")
log.info(
f"FlareCleanupTask will clear {non_open_pulls_with_flare_in_archive.count()} Archive flares"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this .count() is a query on its own, which seems a bit unreasonable just for a log line?

Comment on lines +20 to +22
mock_archive_service = mocker.patch(
"shared.django_apps.utils.model_utils.ArchiveService"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of mocking the ArchiveService, you could use the mock_storage fixture (I believe that should be hooked up to the shared archive service).

That way, you can actually store files (in memory), and assert that those files are truely being deleted from the storage.

Copy link

github-actions bot commented Dec 28, 2024

This PR includes changes to shared. Please review them here: codecov/shared@2674ae9...741151b

@codecov-qa
Copy link

codecov-qa bot commented Dec 28, 2024

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1769 1 1768 1
View the top 1 failed tests by shortest run time
tasks/tests/unit/test_flare_cleanup.py::TestFlareCleanupTask::test_successful_run
Stack Traces | 0.309s run time
self = <worker.tasks.tests.unit.test_flare_cleanup.TestFlareCleanupTask object at 0x7f4e421ca210>
transactional_db = None
mocker = <pytest_mock.plugin.MockFixture object at 0x7f4e3be52510>

    def test_successful_run(self, transactional_db, mocker):
        mock_logs = mocker.patch("logging.Logger.info")
        mock_archive_service = mocker.patch(
            "shared.django_apps.utils.model_utils.ArchiveService"
        )
        archive_value_for_flare = {"some": "data"}
        mock_archive_service.return_value.read_file.return_value = json.dumps(
            archive_value_for_flare
        )
        mock_path = ".../to/written/object"
        mock_archive_service.return_value.write_json_data_to_storage.return_value = (
            mock_path
        )
        mock_archive_service_in_task = mocker.patch(
            "tasks.flare_cleanup.ArchiveService"
        )
        mock_archive_service_in_task.return_value.delete_file.return_value = None
    
        local_value_for_flare = {"test": "test"}
        open_pull_with_local_flare = PullFactory(
            state=PullStates.OPEN.value,
            _flare=local_value_for_flare,
            repository=RepositoryFactory(),
        )
>       assert open_pull_with_local_flare.flare == local_value_for_flare
E       AssertionError: assert {} == {'test': 'test'}
E         
E         Right contains 1 more item:
E         {'test': 'test'}
E         Use -v to get more diff

.../tests/unit/test_flare_cleanup.py:42: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Copy link

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1769 1 1768 1
View the top 1 failed tests by shortest run time
tasks/tests/unit/test_flare_cleanup.py::TestFlareCleanupTask::test_successful_run
Stack Traces | 0.309s run time
self = <worker.tasks.tests.unit.test_flare_cleanup.TestFlareCleanupTask object at 0x7f4e421ca210>
transactional_db = None
mocker = <pytest_mock.plugin.MockFixture object at 0x7f4e3be52510>

    def test_successful_run(self, transactional_db, mocker):
        mock_logs = mocker.patch("logging.Logger.info")
        mock_archive_service = mocker.patch(
            "shared.django_apps.utils.model_utils.ArchiveService"
        )
        archive_value_for_flare = {"some": "data"}
        mock_archive_service.return_value.read_file.return_value = json.dumps(
            archive_value_for_flare
        )
        mock_path = ".../to/written/object"
        mock_archive_service.return_value.write_json_data_to_storage.return_value = (
            mock_path
        )
        mock_archive_service_in_task = mocker.patch(
            "tasks.flare_cleanup.ArchiveService"
        )
        mock_archive_service_in_task.return_value.delete_file.return_value = None
    
        local_value_for_flare = {"test": "test"}
        open_pull_with_local_flare = PullFactory(
            state=PullStates.OPEN.value,
            _flare=local_value_for_flare,
            repository=RepositoryFactory(),
        )
>       assert open_pull_with_local_flare.flare == local_value_for_flare
E       AssertionError: assert {} == {'test': 'test'}
E         
E         Right contains 1 more item:
E         {'test': 'test'}
E         Use -v to get more diff

.../tests/unit/test_flare_cleanup.py:42: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants