-
Notifications
You must be signed in to change notification settings - Fork 9
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
Conversation
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 | ||
|
||
|
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.
Nothing ever called this code.
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. |
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.
should this be more explicitly named that it processed only Twilio delivery statuses?
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.
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')) |
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.
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.
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.
@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 = ( |
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.
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.
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.
Thank you for checking!
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. |
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.
I agree. It should be process_twilio_delivery_status
.
) -> dict: | ||
def get_notification_platform_status( | ||
provider: SmsClient, | ||
body: str | dict[str, str], |
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.
You call this upstream as sqs_message.get('body')
, which implies it might be None.
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.
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}') |
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.
raise NonRetryableException(f'Found {type(e).__name__}, {UNABLE_TO_TRANSLATE}') | |
raise NonRetryableException(UNABLE_TO_TRANSLATE) from e |
app/clients/sms/aws_pinpoint.py
Outdated
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 = { |
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.
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.
app/clients/sms/aws_pinpoint.py
Outdated
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 |
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.
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), | |
) |
app/dao/notifications_dao.py
Outdated
@@ -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. |
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.
change function name
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 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)) |
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.
Slightly more performant:
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)
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.
Removed. It's not used.
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.
All of the constants defined as lists should instead be tuples, which are more performant.
|
||
def confirm_subscription(confirmation_request): |
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.
Annotation? It quacks like a dictionary.
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.
Letter code, not updating.
@@ -2462,6 +2467,23 @@ def mock_va_profile_response(): | |||
return json.load(f) | |||
|
|||
|
|||
@pytest.fixture | |||
def x_minutes_ago(): |
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.
You created a function 5_minutes_ago
somewhere above. Should you use this instead?
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.
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.
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.
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) |
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.
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.
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.
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) |
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.
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.
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.
Reverted this change and the service_callback_types change. Validation errors were not backwards compatible.
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.
I don't insist on changing anything in this file, but didn't we say we're going to use "|" rather than "Union"?
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.
Description
Refactored
process_delivery_status
andprocess_pinpoint_results
. The code would updatestatus
orstatus_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