import json import re from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from functools import partial from typing import Any, List, Tuple from notification_service.db import NotificationDB, NotificationTable from notification_service.entities import NotificationStatusEnum from notification_service.message_broker import SNSService import pytz # type: ignore from datadog.dogstatsd.base import statsd from smart_open import smart_open from smelog.factory import SmeBoundLogger class NotificationPublisher: def __init__( self, db: NotificationDB, logger: SmeBoundLogger, sns_service: SNSService, notification_ttl: int, thread_counts: int ): self._db = db self._logger = logger self._sns_service = sns_service self._notification_ttl = notification_ttl self._thread_counts = thread_counts def run(self) -> None: # pylint: disable=too-many-locals notifications = self._db.get_ready_notification() for notification in notifications: self._logger.info( 'Start processing notification', notification_id=notification.notification_id ) if self._check_timeout(notification) is True: continue self._db.update_notification( notification_row=notification, send_status=NotificationStatusEnum.IN_PROGRESS.value, send_attempts=notification.send_attempts + 1 ) set_notification_failed = partial( self._db.update_notification, notification_row=notification, send_status=NotificationStatusEnum.FAILED.value, ) playlist_track_ntfs, playlist_ntfs = self._get_data_from_s3( notification=notification, set_notification_failed=set_notification_failed, ) playlist_success_sent, _ = self._sent_notification_to_sns( notification_type='playlist', notification_id=notification.notification_id, data=playlist_ntfs, set_notification_failed=set_notification_failed ) plst_track_success_sent, _ = self._sent_notification_to_sns( notification_type='playlist_track', notification_id=notification.notification_id, data=playlist_track_ntfs, set_notification_failed=set_notification_failed ) self._logger.info( 'Finish processing notification', notification_id=notification.notification_id ) self._db.update_notification( notification_row=notification, send_status=NotificationStatusEnum.COMPLETED.value, sent_playlists_count=playlist_success_sent, sent_tracks_playlists_count=plst_track_success_sent, ) def _check_timeout(self, notification: NotificationTable) -> bool: now = datetime.now(tz=pytz.utc) sf_load_completed_at = notification.sf_load_completed_at.astimezone(pytz.utc) dtf_load_completed_at = notification.dtf_load_completed_at.astimezone(pytz.utc) sf_expired = (now - sf_load_completed_at).total_seconds() / 3600 > self._notification_ttl dtf_expired = (now - dtf_load_completed_at).total_seconds() / 3600 > self._notification_ttl if sf_expired or dtf_expired or notification.send_attempts >= 3: self._db.update_notification( notification_row=notification, send_status=NotificationStatusEnum.TIMED_OUT.value ) return True dtf_last_fact_dt = datetime.fromtimestamp(notification.dtf_last_fact_id).astimezone( pytz.utc ) if dtf_last_fact_dt + timedelta(hours=self._notification_ttl) < now: self._db.update_notification( notification_row=notification, send_status=NotificationStatusEnum.TIMED_OUT.value ) return True return False def _get_data_from_s3( self, notification: NotificationTable, set_notification_failed: Any ) -> Tuple: try: playlist_track_ntfs = self._get_notification_from_s3( s3_path=notification.event_path, filename='playlist_track_event_history.json' ) playlist_ntfs = self._get_notification_from_s3( s3_path=notification.event_path, filename='playlist_event_history.json' ) statsd.increment( metric='notification-service.got_playlist_track_notifications_from_s3', value=len(playlist_track_ntfs) ) statsd.increment( metric='notification-service.got_playlist_notifications_from_s3', value=len(playlist_ntfs) ) except Exception as exc: # pylint: disable=broad-except self._logger.error( f'SNS Service failed to get notification from S3. ' f'Notification id {notification.notification_id}', exc=str(exc) ) set_notification_failed() return (), () else: self._logger.info( 'Notification service got data from S3. ' f'Playlist tracks - {len(playlist_track_ntfs)}' f'Playlists - {len(playlist_ntfs)}' ) return playlist_track_ntfs, playlist_ntfs def _sent_notification_to_sns( self, notification_type: str, notification_id: int, data: List, set_notification_failed: Any ) -> Tuple: sent_count = 0 failed_count = 0 try: with ThreadPoolExecutor(max_workers=self._thread_counts) as executor: futures = [ executor.submit(self._sns_service.send_batch_msgs, batch, data.index(batch)) for batch in data ] for future in futures: success, failed = future.result() sent_count += success failed_count += failed except Exception as exc: # pylint: disable=broad-except self._logger.error( f'SNS Service sent {notification_type} notification failed. ' f'Notification id {notification_id}', exc=str(exc) ) set_notification_failed() raise exc self._logger.info( f'The process of sending {notification_type} data was finished.', sent_count=sent_count, failed_count=failed_count ) return sent_count, failed_count @staticmethod def _get_notification_from_s3(s3_path: str, filename: str) -> List: try: file_res = smart_open(f'{s3_path}{filename}') except Exception as exc: if 'The specified key does not exist' in str(exc): return [] raise exc file_data = file_res.read().decode('utf-8').replace('undefined', 'null') parsed_jsons = [] for data in re.findall(r'\{\"events\"\:(.+)\}\\n', file_data): parsed_jsons.append(json.loads(data)) if not parsed_jsons: for data in re.findall(r'\{\"events\"\:(.+)\}', file_data): parsed_jsons.append(json.loads(data)) return parsed_jsons