def handler(event, context): verify_deploy_stage() kinesis = boto3.client("kinesis") stage = os.environ["STAGE"] idempotency_checker = IdempotencyChecker() inbound_commands = [ _make_inbound_command(record) for record in event["Records"] ] for command in inbound_commands: if command.command_type == InboundCommandType.INBOUND_SMS: if not idempotency_checker.already_processed( command.sequence_number, IDEMPOTENCY_REALM): logging.info( f"Logging an INBOUND_SMS message in the message log") twilio_webhook = command.payload["twilio_webhook"] kinesis.put_record( Data=json.dumps({ "type": "INBOUND_SMS", "payload": twilio_webhook }), PartitionKey=command.payload["From"], StreamName=f"message-log-{stage}", ) idempotency_checker.record_as_processed( command.sequence_number, IDEMPOTENCY_REALM, IDEMPOTENCY_EXPIRATION_MINUTES) return {"statusCode": 200}
class ReminderTriggerer: def __init__(self, **kwargs): self.stage = os.environ.get("STAGE") self.drill_progress_repo = self._get_drill_progress_repo() self.command_publisher = CommandPublisher() self.idempotency_checker = IdempotencyChecker() def _get_drill_progress_repo(self): return DrillProgressRepository() def trigger_reminders(self): drill_instances = self.drill_progress_repo.get_incomplete_drills( inactive_for_minutes_floor=REMINDER_TRIGGER_FLOOR_MINUTES, inactive_for_minutes_ceil=REMINDER_TRIGGER_CEIL_MINUTES, ) for drill_instance in drill_instances: idempotency_key = ( f"{drill_instance.drill_instance_id}-{drill_instance.current_prompt_slug}" ) if self.idempotency_checker.already_processed(idempotency_key, IDEMPOTENCY_REALM): continue # The dialog agent wont send a reminder for the same drill/prompt combo twice # publishing to the stream twice should be avoided, but isn't a big deal. self.command_publisher.publish_trigger_reminder_commands([drill_instance]) self.idempotency_checker.record_as_processed( idempotency_key, IDEMPOTENCY_REALM, IDEMPOTENCY_EXPIRATION_MINUTES )
def setUp(self) -> None: os.environ["STAGE"] = "test" self.idempotency_checker = IdempotencyChecker( region_name="us-west-2", endpoint_url="http://localhost:9000", aws_access_key_id="fake-key", aws_secret_access_key="fake-secret", ) self.idempotency_checker.drop_and_recreate_table()
def handler(event: dict, context: dict) -> dict: verify_deploy_stage() kinesis = get_boto3_client("kinesis") stage = os.environ["STAGE"] idempotency_checker = IdempotencyChecker() form = extract_form(event) if not is_signature_valid(event, form, stage): logging.warning("signature validation failed") return {"statusCode": 403} idempotency_key = event["headers"]["I-Twilio-Idempotency-Token"] if idempotency_checker.already_processed(idempotency_key, IDEMPOTENCY_REALM): logging.info( f"Already processed webhook with idempotency key {idempotency_key}. Skipping." ) return {"statusCode": 200} if "MessageStatus" in form: logging.info( f"Outbound message to {form['To']}: Recording STATUS_UPDATE in message log" ) kinesis.put_record( Data=json.dumps({ "type": "STATUS_UPDATE", "received_at": datetime.now(UTC).isoformat(), "payload": form, }), PartitionKey=form["To"], StreamName=f"message-log-{stage}", ) else: logging.info(f"Inbound message from {form['From']}: '{form['Body']}'") CommandPublisher().publish_process_sms_command(form["From"], form["Body"], form) idempotency_checker.record_as_processed(idempotency_key, IDEMPOTENCY_REALM, IDEMPOTENCY_EXPIRATION_MINUTES) return { "statusCode": 200, "headers": { "content-type": "application/xml" }, "body": str(MessagingResponse()), }
def setUp(self): os.environ["STAGE"] = "test" self.drill_progress_repo = DrillProgressRepository( db.get_test_sqlalchemy_engine) self.drill_progress_repo.drop_and_recreate_tables_testing_only() self.phone_number = "123456789" self.user_id = self.drill_progress_repo._create_or_update_user( DialogEventBatch( events=[ UserValidated(self.phone_number, UserProfile(True), CodeValidationPayload(True)) ], phone_number=self.phone_number, seq="0", batch_id=uuid.uuid4(), ), None, self.drill_progress_repo.engine, ) drill_db_patch = patch( "stopcovid.drill_progress.trigger_reminders.ReminderTriggerer._get_drill_progress_repo", return_value=self.drill_progress_repo, ) drill_db_patch.start() self.addCleanup(drill_db_patch.stop) self.idempotency_checker = IdempotencyChecker( region_name="us-west-2", endpoint_url="http://localhost:9000", aws_access_key_id="fake-key", aws_secret_access_key="fake-secret", ) self.idempotency_checker.drop_and_recreate_table() idempotency_patch = patch( "stopcovid.drill_progress.trigger_reminders.IdempotencyChecker", return_value=self.idempotency_checker, ) idempotency_patch.start() self.addCleanup(idempotency_patch.stop)
class TestIdempotency(unittest.TestCase): def setUp(self) -> None: os.environ["STAGE"] = "test" self.idempotency_checker = IdempotencyChecker( region_name="us-west-2", endpoint_url="http://localhost:9000", aws_access_key_id="fake-key", aws_secret_access_key="fake-secret", ) self.idempotency_checker.drop_and_recreate_table() def test_idempotency(self): self.assertFalse( self.idempotency_checker.already_processed("idempotency", "realm1")) self.idempotency_checker.record_as_processed("idempotency", "realm1", 5) self.assertTrue( self.idempotency_checker.already_processed("idempotency", "realm1"))
def __init__(self, **kwargs): self.stage = os.environ.get("STAGE") self.drill_progress_repo = self._get_drill_progress_repo() self.command_publisher = CommandPublisher() self.idempotency_checker = IdempotencyChecker()
class TestReminderTriggers(unittest.TestCase): def setUp(self): os.environ["STAGE"] = "test" self.drill_progress_repo = DrillProgressRepository( db.get_test_sqlalchemy_engine) self.drill_progress_repo.drop_and_recreate_tables_testing_only() self.phone_number = "123456789" self.user_id = self.drill_progress_repo._create_or_update_user( DialogEventBatch( events=[ UserValidated(self.phone_number, UserProfile(True), CodeValidationPayload(True)) ], phone_number=self.phone_number, seq="0", batch_id=uuid.uuid4(), ), None, self.drill_progress_repo.engine, ) drill_db_patch = patch( "stopcovid.drill_progress.trigger_reminders.ReminderTriggerer._get_drill_progress_repo", return_value=self.drill_progress_repo, ) drill_db_patch.start() self.addCleanup(drill_db_patch.stop) self.idempotency_checker = IdempotencyChecker( region_name="us-west-2", endpoint_url="http://localhost:9000", aws_access_key_id="fake-key", aws_secret_access_key="fake-secret", ) self.idempotency_checker.drop_and_recreate_table() idempotency_patch = patch( "stopcovid.drill_progress.trigger_reminders.IdempotencyChecker", return_value=self.idempotency_checker, ) idempotency_patch.start() self.addCleanup(idempotency_patch.stop) def _get_incomplete_drill_with_last_prompt_started_min_ago(self, min_ago): return make_drill_instance( current_prompt_start_time=datetime.datetime.now( datetime.timezone.utc) - datetime.timedelta(minutes=min_ago), completion_time=None, user_id=self.user_id, ) def test_reminder_triggerer_ignores_drills_below_inactivity_threshold( self, publish_mock): drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago( 10) self.drill_progress_repo._save_drill_instance(drill_instance) ReminderTriggerer().trigger_reminders() publish_mock.assert_not_called() def test_reminder_triggerer_ignores_drills_above_inactivity_threshold( self, publish_mock): two_day_old_drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago( 60 * 24 * 2) self.drill_progress_repo._save_drill_instance( two_day_old_drill_instance) ReminderTriggerer().trigger_reminders() publish_mock.assert_not_called() def test_reminder_triggerer_triggers_reminder_and_persists_idempotency_key( self, publish_mock): drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago( 5 * 60) self.drill_progress_repo._save_drill_instance(drill_instance) expected_idempotency_key = ( f"{drill_instance.drill_instance_id}-{drill_instance.current_prompt_slug}" ) self.assertFalse( self.idempotency_checker.already_processed( expected_idempotency_key, IDEMPOTENCY_REALM)) ReminderTriggerer().trigger_reminders() publish_mock.assert_called_once_with([drill_instance]) self.assertTrue( self.idempotency_checker.already_processed( expected_idempotency_key, IDEMPOTENCY_REALM)) def test_does_not_double_trigger_reminders(self, publish_mock): drill_instance = self._get_incomplete_drill_with_last_prompt_started_min_ago( 5 * 60) self.drill_progress_repo._save_drill_instance(drill_instance) ReminderTriggerer().trigger_reminders() publish_mock.assert_called_once_with([drill_instance]) publish_mock.reset_mock() ReminderTriggerer().trigger_reminders() publish_mock.assert_not_called()