from datetime import datetime, timedelta from typing import Any, List, Optional from notification_service.entities import DTFStatusEnum, NotificationStatusEnum from datadog.dogstatsd.base import statsd from db_schema.postgres.connection import Connection from smelog.factory import SmeBoundLogger from sqlalchemy import Boolean, Column, Integer, Text, text from sqlalchemy.dialects.postgresql import TIMESTAMP from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class NotificationTable(Base): # type: ignore # pylint:disable=too-few-public-methods __tablename__ = 'notifications' __table_args__ = {'schema': 'notification'} notification_id = Column(Integer, nullable=False, primary_key=True, autoincrement=True) uow_id = Column(Integer, nullable=False) send_status = Column(Text, nullable=True) event_path = Column(Text, nullable=True) is_data_ready = Column(Boolean, nullable=False) data_load_status = Column(Text, nullable=False) send_attempts = Column(Integer, nullable=False) sent_playlists_count = Column(Integer, nullable=True) sent_tracks_playlists_count = Column(Integer, nullable=True) dtf_load_completed_at = Column(TIMESTAMP(timezone=True), nullable=True) dtf_first_fact_id = Column(Integer, nullable=True) dtf_last_fact_id = Column(Integer, nullable=True) sf_load_completed_at = Column(TIMESTAMP(timezone=True), nullable=True) created_at = Column(TIMESTAMP(timezone=True), server_default=text('now()'), nullable=False) updated_at = Column(TIMESTAMP(timezone=True), nullable=True) class NotificationDB: def __init__(self, conn: Connection, logger: SmeBoundLogger) -> None: self.logger = logger self.conn = conn def create_notification(self, uow_id: int, **kwargs: Any) -> NotificationTable: try: notification = NotificationTable( uow_id=uow_id, send_status=NotificationStatusEnum.CREATED.value, send_attempts=0, sent_playlists_count=0, sent_tracks_playlists_count=0, ) for column, value in kwargs.items(): setattr(notification, column, value) self.conn.session.add(notification) self.conn.session.commit() statsd.increment('notification-service.created_meta_notification') return notification except SQLAlchemyError as exc: self.logger.exception( 'Failed to create notification', exc, ) raise def update_notification( self, notification_row: NotificationTable, **kwargs: Any ) -> NotificationTable: try: for column, value in kwargs.items(): setattr(notification_row, column, value) notification_row.updated_at = datetime.utcnow() self.conn.session.commit() return notification_row except SQLAlchemyError as exc: self.logger.exception( 'Failed to update notification', exc, ) raise def get_notification(self, uow_id: int) -> Optional[NotificationTable]: try: notification_row: NotificationTable = self.conn.session.query(NotificationTable) notification_row = notification_row.filter(NotificationTable.uow_id == uow_id) return notification_row.one_or_none() except SQLAlchemyError as exc: self.logger.exception( 'Failed to get notification', exc, ) raise def get_ready_notification(self) -> List[NotificationTable]: try: notification_row: NotificationTable = self.conn.session.query(NotificationTable) notification_row = notification_row.filter( NotificationTable.is_data_ready.is_(True), NotificationTable.data_load_status == DTFStatusEnum.IMPORT_COMPLETED.value, NotificationTable.send_status.in_( [NotificationStatusEnum.CREATED.value, NotificationStatusEnum.FAILED.value] ), NotificationTable.send_attempts <= 4 ) ready_notifications = notification_row.all() statsd.gauge( 'notification-service.ready_meta_notifications_to_send', len(ready_notifications) ) return ready_notifications except SQLAlchemyError as exc: self.logger.exception( 'Failed to get notification', exc, ) raise def update_stuck_notifications(self) -> None: notification_row: NotificationTable = self.conn.session.query(NotificationTable) notification_row = notification_row.filter( NotificationTable.send_status == NotificationStatusEnum.IN_PROGRESS.value, NotificationTable.updated_at < (datetime.now() - timedelta(hours=2)) ) notification_rows = notification_row.all() failed_uows = 0 timed_out_uows = 0 for notification in notification_rows: if notification.send_attempts < 3: failed_uows += 1 notification.send_status = NotificationStatusEnum.FAILED.value else: timed_out_uows += 1 notification.send_status = NotificationStatusEnum.TIMED_OUT.value statsd.gauge('notification-service.failed_stuck_meta_notifications', failed_uows) statsd.gauge('notification-service.timeout_stuck_meta_notifications', timed_out_uows) self.conn.session.commit() def disable_stuck_created_notifications(self) -> None: notification_row: NotificationTable = self.conn.session.query(NotificationTable) notification_row = notification_row.filter( NotificationTable.send_status == NotificationStatusEnum.CREATED.value, NotificationTable.created_at < (datetime.now() - timedelta(hours=24)) ) notification_rows = notification_row.all() for notification in notification_rows: notification.send_status = NotificationStatusEnum.TIMED_OUT.value statsd.gauge( 'notification-service.timeout_stuck_created_meta_notifications', len(notification_rows) ) self.conn.session.commit()