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

#2011 - Eliminate Null Status Reason #2083

Merged
merged 17 commits into from
Oct 30, 2024

Conversation

k-macmillan
Copy link
Member

@k-macmillan k-macmillan commented Oct 25, 2024

Description

Refactored process_delivery_status and process_pinpoint_results. The code would update status or status_reason in multiple functions, but we need the branching logic removed from the app and added to the query, so that has been done.

The query now accepts any transition, and the app code determines from a given state, what state we would like to be in. The app code passes it to the query, so if an update came between the two (race condition), the DB won't transition to an invalid state. If a new update comes in there is no way for it to go "backwards" in status accidentally.

issue #2011

How Has This Been Tested?

New unit tests written (over 100, some new, some parameterized). Unit tests cover every possible status transition. Deployed, regression passed.

Checklist

  • I have assigned myself to this PR
  • PR has an appropriate title: #9999 - What the thing does
  • PR has a detailed description, including links to specific documentation
  • I have added the appropriate labels to the PR.
  • I did not remove any parts of the template, such as checkboxes even if they are not used
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to any documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works. Testing guidelines
  • I have ensured the latest main is merged into my branch and all checks are green prior to review
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • The ticket was moved into the DEV test column when I began testing this change

@k-macmillan k-macmillan self-assigned this Oct 25, 2024
Comment on lines -112 to -122
def _decide_permanent_temporary_failure(
current_status,
status,
):
# Firetext will send pending, then send either success or fail.
# If we go from pending to delivered we need to set failure type as temporary-failure
if current_status == NOTIFICATION_PENDING and status == NOTIFICATION_PERMANENT_FAILURE:
status = NOTIFICATION_TEMPORARY_FAILURE
return status


Copy link
Member Author

Choose a reason for hiding this comment

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

Nothing ever called this code.

@k-macmillan k-macmillan marked this pull request as ready for review October 28, 2024 19:53
@k-macmillan k-macmillan requested a review from a team as a code owner October 28, 2024 19:53
Comment on lines 35 to +40
def process_delivery_status(
self,
self: Task,
event: CeleryEvent,
) -> bool:
) -> None:
"""
This is a Celery task for updating the delivery status of a notification.
This is a Celery task for updating the delivery status of a Twilio notification.

Choose a reason for hiding this comment

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

should this be more explicitly named that it processed only Twilio delivery statuses?

Copy link
Member

Choose a reason for hiding this comment

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

I agree. It should be process_twilio_delivery_status.


body = sqs_message.get('body')
current_app.logger.info('retrieved delivery status body: %s', body)
notification_platform_status: SmsStatusRecord = get_notification_platform_status(provider, sqs_message.get('body'))
Copy link

@MackHalliday MackHalliday Oct 29, 2024

Choose a reason for hiding this comment

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

NIT: (and likely not apart of this PR) The code path for get_notification_platform_status is dead end and will result in a NotImplementedError error.

Copy link

@MackHalliday MackHalliday Oct 29, 2024

Choose a reason for hiding this comment

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

@k-macmillan I'd like to talk about how the notification_platform_status variable is being used further down the code path. Is this being populated as expected?

As I follow the code - this will hit a NotImplementedError.

incoming_status_reason=new_status_reason,
)
if notification_type == SMS_TYPE:
stmt = (
Copy link

@MackHalliday MackHalliday Oct 29, 2024

Choose a reason for hiding this comment

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

I spent some time looking at this query to see if the efficient could be improved. After some review, I believe the current query is sufficient.

I looked at updating sms_conditions to return a SQL statement that uses CASE instead of a series of OR statements. But could not find anything conclusive that CASE is more performative.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for checking!

Comment on lines 35 to +40
def process_delivery_status(
self,
self: Task,
event: CeleryEvent,
) -> bool:
) -> None:
"""
This is a Celery task for updating the delivery status of a notification.
This is a Celery task for updating the delivery status of a Twilio notification.
Copy link
Member

Choose a reason for hiding this comment

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

I agree. It should be process_twilio_delivery_status.

) -> dict:
def get_notification_platform_status(
provider: SmsClient,
body: str | dict[str, str],
Copy link
Member

Choose a reason for hiding this comment

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

You call this upstream as sqs_message.get('body'), which implies it might be None.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good callout. Wouldn't have caused an issue (caught and raised app/clients/sms/twilio.py:254) but could have been confusing.

Modified the upstream so the function signature doesn't need to change.

raise AutoRetryException(f'Found {type(e).__name__}, autoretrying...')
current_app.logger.exception('Unable to decode the incoming pinpoint message')
statsd_client.incr('clients.sms.pinpoint.status_update.error')
raise NonRetryableException(f'Found {type(e).__name__}, {UNABLE_TO_TRANSLATE}')
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise NonRetryableException(f'Found {type(e).__name__}, {UNABLE_TO_TRANSLATE}')
raise NonRetryableException(UNABLE_TO_TRANSLATE) from e

from app.exceptions import InvalidProviderException


class AwsPinpointException(SmsClientResponseException):
pass


# This is not an exhaustive list of all possible record statuses. See the documentation linked below.
_sms_record_status_mapping = {
Copy link
Member

Choose a reason for hiding this comment

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

In general, when you declare things like this, please comment. I think this is a mapping to (status, status_reason), but I would have to see how it's used to infer if that's correct. I shouldn't need to do that.

Comment on lines 212 to 223
notification_platform_status = SmsStatusRecord(
None,
pinpoint_attributes['message_id'],
status,
status_reason,
PINPOINT_PROVIDER,
pinpoint_attributes['number_of_message_parts'],
delivery_status_message['metrics']['price_in_millicents_usd'],
datetime.fromtimestamp(delivery_status_message['event_timestamp'] / 1000),
)

return notification_platform_status
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
notification_platform_status = SmsStatusRecord(
None,
pinpoint_attributes['message_id'],
status,
status_reason,
PINPOINT_PROVIDER,
pinpoint_attributes['number_of_message_parts'],
delivery_status_message['metrics']['price_in_millicents_usd'],
datetime.fromtimestamp(delivery_status_message['event_timestamp'] / 1000),
)
return notification_platform_status
return SmsStatusRecord(
None,
pinpoint_attributes['message_id'],
status,
status_reason,
PINPOINT_PROVIDER,
pinpoint_attributes['number_of_message_parts'],
delivery_status_message['metrics']['price_in_millicents_usd'],
datetime.fromtimestamp(delivery_status_message['event_timestamp'] / 1000),
)

@@ -341,7 +446,7 @@ def dao_update_notification_by_id(
**kwargs,
) -> Optional[Notification]:
"""
Update the notification by ID, ensure kwargs paramaters are named appropriately according to the notification model.
Update the SMS notification by ID, ensure kwargs paramaters are named appropriately according to the notification model.
Copy link
Member

Choose a reason for hiding this comment

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

change function name

Copy link
Member Author

Choose a reason for hiding this comment

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

The docstring was wrong, oops.

app/models.py Outdated
SERVICE_CALLBACK_TYPES = [DELIVERY_STATUS_CALLBACK_TYPE, COMPLAINT_CALLBACK_TYPE, INBOUND_SMS_CALLBACK_TYPE]

# models.py only constants
NOTIFICATION_STATUS_TYPES_NON_BILLABLE = list(set(NOTIFICATION_STATUS_TYPES) - set(NOTIFICATION_STATUS_TYPES_BILLABLE))
Copy link
Member

Choose a reason for hiding this comment

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

Slightly more performant:

Suggested change
NOTIFICATION_STATUS_TYPES_NON_BILLABLE = list(set(NOTIFICATION_STATUS_TYPES) - set(NOTIFICATION_STATUS_TYPES_BILLABLE))
NOTIFICATION_STATUS_TYPES_NON_BILLABLE = tuple(frozenset(NOTIFICATION_STATUS_TYPES) - frozenset(NOTIFICATION_STATUS_TYPES_BILLABLE))
>>> tuple(frozenset([1,2,3]) - frozenset([4,5,6]))
(1, 2, 3)

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed. It's not used.

Copy link
Member

Choose a reason for hiding this comment

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

All of the constants defined as lists should instead be tuples, which are more performant.


def confirm_subscription(confirmation_request):
Copy link
Member

Choose a reason for hiding this comment

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

Annotation? It quacks like a dictionary.

Copy link
Member Author

Choose a reason for hiding this comment

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

Letter code, not updating.

@@ -2462,6 +2467,23 @@ def mock_va_profile_response():
return json.load(f)


@pytest.fixture
def x_minutes_ago():
Copy link
Member

Choose a reason for hiding this comment

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

You created a function 5_minutes_ago somewhere above. Should you use this instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Got rid of five_minutes_ago. I meant to replace them all with x_minutes_ago because it's more versatile. Cut out the five_minutes_ago method.

Copy link

@cris-oddball cris-oddball left a comment

Choose a reason for hiding this comment

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

Found where the regression failed.

app/constants.py Outdated
DELIVERY_STATUS_CALLBACK_TYPE = 'delivery_status'
COMPLAINT_CALLBACK_TYPE = 'complaint'
INBOUND_SMS_CALLBACK_TYPE = 'inbound_sms'
SERVICE_CALLBACK_TYPES = (DELIVERY_STATUS_CALLBACK_TYPE, COMPLAINT_CALLBACK_TYPE, INBOUND_SMS_CALLBACK_TYPE)

Choose a reason for hiding this comment

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

This fails the regression for POST payloads with an invalid value for callback_type. The code now returns a tuple in the error message, not a list. We can change the regression or we can change this back to a list. Please let me know.

Copy link
Member Author

Choose a reason for hiding this comment

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

These two are only used in validation schema and the validation error raised used string interpolation to add the tuple/list to the validation error, so if we added or removed any it'd also break. Since it's only used in one spot on an error I cast the tuple to a list to maintain backwards compatibility. Also walked ALL validation errors raised in the code, looking for any more and none were found (other than the channels you pointed out).

app/constants.py Outdated
SERVICE_CALLBACK_TYPES = (DELIVERY_STATUS_CALLBACK_TYPE, COMPLAINT_CALLBACK_TYPE, INBOUND_SMS_CALLBACK_TYPE)
WEBHOOK_CHANNEL_TYPE = 'webhook'
QUEUE_CHANNEL_TYPE = 'queue'
CALLBACK_CHANNEL_TYPES = (WEBHOOK_CHANNEL_TYPE, QUEUE_CHANNEL_TYPE)

Choose a reason for hiding this comment

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

This fails the regression for POST payloads with an invalid value for callback_channel. The code now returns a tuple in the error message, not a list. We can change the regression or we can change this back to a list. Please let me know.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reverted this change and the service_callback_types change. Validation errors were not backwards compatible.

Copy link
Member

Choose a reason for hiding this comment

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

I don't insist on changing anything in this file, but didn't we say we're going to use "|" rather than "Union"?

Copy link
Member Author

Choose a reason for hiding this comment

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

We lost consistency at some point.
image

For va-enp-api we are not using Union nor Optional.

@k-macmillan k-macmillan merged commit 0a5118a into main Oct 30, 2024
13 checks passed
@k-macmillan k-macmillan deleted the 2011-eliminate-null-status-reason branch October 30, 2024 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants