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

3169 move notifications presteps to one task #989

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

adrian-codecov
Copy link
Contributor

@adrian-codecov adrian-codecov commented Jan 3, 2025

So, this PR stems from us having many notification conditions spread out in different tasks (finisher, notify) and this is my take for it. I decided to create a task, notification_orchestrator, that's meant to be a centralized place for doing checks that determine whether we'd notify or not, rather than doing it when we're already in the notify task (and expecting to notify). The idea is for any codecov product to tap into this orchestrator if they desire to do so, although this first pass is targeted mostly for the coverage case.

I would recommend solely looking at notification_orchestrator.py and giving feedback there. The changes in upload_finisher.py and notify.py task are mostly deletions - these are the changes that now belong in the orchestrator task. I have not yet tested this orchestrator task fully, so this is moreso a pass about it capturing the logic well and if it seems a reasonable/readable approach for notifying. I'm easy with renaming and whatnot, just let me know what you think :)

I'm happy for this PR to just be adding the task, and in a future PR connecting it to the finisher/notify task (I'd modify this PR to revert the changes in those two files as well).

I left some inline comments as questions and I'll leave some more as PR comments. Please let me know about this when you get a chance. After the 1st pass, I'll work on the tests as well.

Legal Boilerplate

Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. In 2022 this entity acquired Codecov and as result Sentry is going to need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.

empty_upload=empty_upload,
**kwargs,
)
# TODO: What should happen when there's a lock error? I think this should change to some retrying mechanism
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm thinking when 2 finishers are done (presumably with different results), one picks up this lock and the other will wait. I suspect the 2nd one should be able to pick up this task

),
)
UploadFlow.log(UploadFlow.NOTIF_LOCK_ERROR)
return {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO for me: I should change this to ShouldCallNotifyResponse

assert commit, "Commit not found in database."
commit_yaml = UserYaml(commit_yaml)

log_extra_dict = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I pass around this log bw functions, but there might be a better strategy to do so, perhaps leveraging the context 👀

Copy link
Contributor

Choose a reason for hiding this comment

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

repoid and commitid are already on the log context if that's been re-enabled

there are no examples of this, but in principle if you have task-specific fields you would like to log you can subclass LogContext, add your own fields, override as_dict(), and call set_log_context(my_ctx) at the start of your task. i think because of dataclass quirks all of your fields must have default values (or you need to rework LogContext to be kw_only=True)

if something truly makes sense on every log across the board you can just add it to the log context itself but i don't think we should cram too much in there. commit_yaml probably doesn't belong there for example; we're never going to search by it and it isn't going to change so we only really need that on one log message

@codecov-staging
Copy link

❌ 12 Tests Failed:

Tests completed Failed Passed Skipped
12 12 0 0
View the top 3 failed tests by shortest run time
tasks/tests/integration/test_http_request_task.py::::tasks.tests.integration.test_http_request_task
Stack Traces | 0s run time
No failure message available
tasks/tests/integration/test_notify_task.py::::tasks.tests.integration.test_notify_task
Stack Traces | 0s run time
No failure message available
tasks/tests/integration/test_status_set_pending_task.py::::tasks.tests.integration.test_status_set_pending_task
Stack Traces | 0s run time
No failure message available

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

@codecov-qa
Copy link

codecov-qa bot commented Jan 3, 2025

❌ 12 Tests Failed:

Tests completed Failed Passed Skipped
12 12 0 0
View the top 3 failed tests by shortest run time
tasks/tests/integration/test_ghm_sync_plans.py::tasks.tests.integration.test_ghm_sync_plans
Stack Traces | 0s run time
ImportError while importing test module '.../tests/integration/test_ghm_sync_plans.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.../local/lib/python3.13/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tasks/__init__.py:34: in <module>
    from tasks.notification_orchestrator import notification_orchestrator_task
tasks/__init__.py:34: in <module>
    from tasks.notification_orchestrator import notification_orchestrator_task
tasks/notification_orchestrator.py:8: in <module>
    from shared.celery_config import (
E   ImportError: cannot import name 'notification_orchestrator_task_name' from 'shared.celery_config' (.../local/lib/python3.13.../site-packages/shared/celery_config.py)
tasks/tests/integration/test_status_set_pending_task.py::tasks.tests.integration.test_status_set_pending_task
Stack Traces | 0s run time
ImportError while importing test module '.../tests/integration/test_status_set_pending_task.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.../local/lib/python3.13/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tasks/__init__.py:34: in <module>
    from tasks.notification_orchestrator import notification_orchestrator_task
tasks/__init__.py:34: in <module>
    from tasks.notification_orchestrator import notification_orchestrator_task
tasks/notification_orchestrator.py:8: in <module>
    from shared.celery_config import (
E   ImportError: cannot import name 'notification_orchestrator_task_name' from 'shared.celery_config' (.../local/lib/python3.13.../site-packages/shared/celery_config.py)
tasks/tests/integration/test_timeseries_backfill.py::tasks.tests.integration.test_timeseries_backfill
Stack Traces | 0s run time
ImportError while importing test module '.../tests/integration/test_timeseries_backfill.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.../local/lib/python3.13/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tasks/__init__.py:34: in <module>
    from tasks.notification_orchestrator import notification_orchestrator_task
tasks/__init__.py:34: in <module>
    from tasks.notification_orchestrator import notification_orchestrator_task
tasks/notification_orchestrator.py:8: in <module>
    from shared.celery_config import (
E   ImportError: cannot import name 'notification_orchestrator_task_name' from 'shared.celery_config' (.../local/lib/python3.13.../site-packages/shared/celery_config.py)

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

Copy link

❌ 12 Tests Failed:

Tests completed Failed Passed Skipped
12 12 0 0
View the top 3 failed tests by shortest run time
tasks/tests/integration/test_ai_pr_review.py::::tasks.tests.integration.test_ai_pr_review
Stack Traces | 0s run time
No failure message available
tasks/tests/integration/test_sync_pull.py::::tasks.tests.integration.test_sync_pull
Stack Traces | 0s run time
No failure message available
tasks/tests/integration/test_upload_e2e.py::::tasks.tests.integration.test_upload_e2e
Stack Traces | 0s run time
No failure message available

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

"countdown": countdown,
"max_retries": max_retries,
},
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should add a UploadFlow log for "reattempting notification" or something

Copy link

github-actions bot commented Jan 3, 2025

❌ 24 Tests Failed:

Tests completed Failed Passed Skipped
24 24 0 0
View the top 3 failed tests by shortest run time
services.tests.test_repository_service
Stack Traces | 0.000s run time
No failure message available
tasks.tests.integration.test_ai_pr_review
Stack Traces | 0.000s run time
No failure message available
tasks.tests.integration.test_ghm_sync_plans
Stack Traces | 0.000s run time
No failure message available

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

Copy link

codecov bot commented Jan 3, 2025

❌ 12 Tests Failed:

Tests completed Failed Passed Skipped
12 12 0 0
View the top 3 failed tests by shortest run time
tasks/tests/integration/test_ghm_sync_plans.py::::tasks.tests.integration.test_ghm_sync_plans
Stack Traces | 0s run time
No failure message available
tasks/tests/integration/test_http_request_task.py::::tasks.tests.integration.test_http_request_task
Stack Traces | 0s run time
No failure message available
tasks/tests/integration/test_send_email_task.py::::tasks.tests.integration.test_send_email_task
Stack Traces | 0s run time
No failure message available

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

# Defines all the logic to consider a notification
should_notify_check_functions = [
partial(
self.upload_processing_checks, processing_results=processing_results
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These types of notifications could in theory happen before we even grab the lock, but leaving them here for now to be consistent with where the rest of the checks get done.

I also don't think the check order matters here, just as long as we make them, but I could be wrong

Copy link
Contributor

Choose a reason for hiding this comment

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

i also assume order doesn't matter here but i would not be shocked if it does :P

don't have to do it now, but it might be a good idea to make each of these functions async and then asyncio.gather() or asyncio.as_completed() them to run them concurrently. could speed up notifications a good amount

Copy link
Contributor

@matt-codecov matt-codecov left a comment

Choose a reason for hiding this comment

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

i didn't 1:1 compare the moved logic to the new logic and there are still TODO comments + open comments form me so i'm not stamping yet, but overall looks great!

Comment on lines +56 to +58
# This is currently not used, but my idea here was to provide it and log this message in switch statements.
# Logging could become a bit less flexible but there's less logging altogether. Thoughts?
message: str
Copy link
Contributor

Choose a reason for hiding this comment

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

how would message be different from reason?

message: str


# TODO: Ask: does this need a throws = (SoftTimeLimitExceeded,)?
Copy link
Contributor

Choose a reason for hiding this comment

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

never noticed this throws= thing before. thiago added one to the notify task in 46c9bc5, looks like it's to tell celery that a certain uncaught exception is not necessarily an error...? interesting

i would say it should be considered an error if this task times out. if it happens a lot we can potentially async-ify the conditions and perform them concurrently, we're not totally helpless

assert commit, "Commit not found in database."
commit_yaml = UserYaml(commit_yaml)

log_extra_dict = {
Copy link
Contributor

Choose a reason for hiding this comment

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

repoid and commitid are already on the log context if that's been re-enabled

there are no examples of this, but in principle if you have task-specific fields you would like to log you can subclass LogContext, add your own fields, override as_dict(), and call set_log_context(my_ctx) at the start of your task. i think because of dataclass quirks all of your fields must have default values (or you need to rework LogContext to be kw_only=True)

if something truly makes sense on every log across the board you can just add it to the log context itself but i don't think we should cram too much in there. commit_yaml probably doesn't belong there for example; we're never going to search by it and it isn't going to change so we only really need that on one log message

Comment on lines -227 to -244
self.app.tasks[pulls_task_name].apply_async(
kwargs=dict(
repoid=repoid,
pullid=pull.pullid,
should_send_notifications=False,
)
)
compared_to = pull.get_comparedto_commit()
if compared_to:
comparison = get_or_create_comparison(
db_session, compared_to, commit
)
db_session.commit()
self.app.tasks[
compute_comparison_task_name
].apply_async(
kwargs=dict(comparison_id=comparison.id)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

these side-effects don't sound notification-related. if they need to happen, they probably belong here

in fact, i wonder if there is a bug here. currently we kick off the notification and then kick off these side-effects. the notification does not wait for the PR to be updated or for the comparison to be computed, it'll just go. does that mean it will have stale information? will it duplicate the work that these tasks are doing asynchronously? we might need to change this to a chain/chord that will kick the notification off after the pull/compare tasks finish

Comment on lines +184 to +185
# ASK: this installation/bot related logic impedes notification if unavailable, but it
# seems correct for it to be part of the notifier task, thoughts?
Copy link
Contributor

Choose a reason for hiding this comment

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

i think the git provider app/token stuff is a tricky gray area

if you imagine a generic notification system, there are roughly three stages:

  • deciding whether to post this kind of notification based on business logic
    • i would say app/token stuff doesn't belong in this stage
  • computing the underlying data for this notification
    • some data (e.g. patch coverage) may require credentials to compute. so the coverage product for example has to decide whether to totally fail without credentials or just skip patch coverage
  • sending the Slack template to Slack, the PR Comment template to the PR Comment, etc
    • the Slack notification doesn't care if we don't have git provider credentials, but the PR comment does

ideally, missing credentials would only skip the things that definitely require credentials and the rest of the system would continue working. and when credentials are missing, that's a misconfiguration on the part of the user and we should alert them to it in the UI but otherwise not treat it as an error

i think your NO task is implementing the first stage of the generic notification system, where app/token stuff shouldn't be a factor. the NotifyTask represents the second and third stages, and cleaning that up to work like i described is a separate matter

# Defines all the logic to consider a notification
should_notify_check_functions = [
partial(
self.upload_processing_checks, processing_results=processing_results
Copy link
Contributor

Choose a reason for hiding this comment

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

i also assume order doesn't matter here but i would not be shocked if it does :P

don't have to do it now, but it might be a good idea to make each of these functions async and then asyncio.gather() or asyncio.as_completed() them to run them concurrently. could speed up notifications a good amount

Comment on lines +365 to +368
# This is technically a different repository_service method than the one used in the notify task. I
# replaced it because the other method is a) deeply intertwined with the notify file and b) it seems
# to be extra specific just for the notification aspect of it, so this should suffice for other checks.
# Please look corroborate this isn't a red flag.
Copy link
Contributor

Choose a reason for hiding this comment

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

is this a note to yourself or to reviewers?

Comment on lines +527 to +528
# Some check on CI skipping from some regex? Idk what this is
if regexp_ci_skip.search(commit.message or ""):
Copy link
Contributor

Choose a reason for hiding this comment

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

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