2024-05-23 10:27:08 +02:00
|
|
|
import arrow
|
|
|
|
import newrelic.agent
|
|
|
|
import psycopg2
|
|
|
|
import select
|
|
|
|
|
|
|
|
from abc import ABC, abstractmethod
|
2024-07-24 16:49:55 +02:00
|
|
|
|
|
|
|
from app.db import Session
|
2024-05-23 10:27:08 +02:00
|
|
|
from app.log import LOG
|
|
|
|
from app.models import SyncEvent
|
|
|
|
from app.events.event_dispatcher import NOTIFICATION_CHANNEL
|
|
|
|
from time import sleep
|
|
|
|
from typing import Callable, NoReturn
|
|
|
|
|
|
|
|
_DEAD_LETTER_THRESHOLD_MINUTES = 10
|
|
|
|
_DEAD_LETTER_INTERVAL_SECONDS = 30
|
|
|
|
|
2024-05-24 10:21:19 +02:00
|
|
|
_POSTGRES_RECONNECT_INTERVAL_SECONDS = 5
|
|
|
|
|
2024-05-23 10:27:08 +02:00
|
|
|
|
|
|
|
class EventSource(ABC):
|
|
|
|
@abstractmethod
|
|
|
|
def run(self, on_event: Callable[[SyncEvent], NoReturn]):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class PostgresEventSource(EventSource):
|
|
|
|
def __init__(self, connection_string: str):
|
2024-05-24 10:21:19 +02:00
|
|
|
self.__connection_string = connection_string
|
|
|
|
self.__connect()
|
2024-05-23 10:27:08 +02:00
|
|
|
|
|
|
|
def run(self, on_event: Callable[[SyncEvent], NoReturn]):
|
2024-05-24 10:21:19 +02:00
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
self.__listen(on_event)
|
|
|
|
except Exception as e:
|
|
|
|
LOG.warn(f"Error listening to events: {e}")
|
|
|
|
sleep(_POSTGRES_RECONNECT_INTERVAL_SECONDS)
|
|
|
|
self.__connect()
|
|
|
|
|
|
|
|
def __listen(self, on_event: Callable[[SyncEvent], NoReturn]):
|
2024-05-23 10:27:08 +02:00
|
|
|
self.__connection.set_isolation_level(
|
|
|
|
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
|
|
|
|
)
|
|
|
|
|
|
|
|
cursor = self.__connection.cursor()
|
|
|
|
cursor.execute(f"LISTEN {NOTIFICATION_CHANNEL};")
|
|
|
|
|
2024-08-20 16:01:48 +02:00
|
|
|
LOG.info("Starting to listen to events")
|
2024-05-23 10:27:08 +02:00
|
|
|
while True:
|
|
|
|
if select.select([self.__connection], [], [], 5) != ([], [], []):
|
|
|
|
self.__connection.poll()
|
|
|
|
while self.__connection.notifies:
|
|
|
|
notify = self.__connection.notifies.pop(0)
|
|
|
|
LOG.debug(
|
|
|
|
f"Got NOTIFY: pid={notify.pid} channel={notify.channel} payload={notify.payload}"
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
webhook_id = int(notify.payload)
|
|
|
|
event = SyncEvent.get_by(id=webhook_id)
|
|
|
|
if event is not None:
|
2024-05-24 10:21:19 +02:00
|
|
|
if event.mark_as_taken():
|
|
|
|
on_event(event)
|
|
|
|
else:
|
|
|
|
LOG.info(
|
|
|
|
f"Event {event.id} was handled by another runner"
|
|
|
|
)
|
2024-05-23 10:27:08 +02:00
|
|
|
else:
|
|
|
|
LOG.info(f"Could not find event with id={notify.payload}")
|
|
|
|
except Exception as e:
|
|
|
|
LOG.warn(f"Error getting event: {e}")
|
2024-07-24 16:49:55 +02:00
|
|
|
Session.close() # Ensure we get a new connection and we don't leave a dangling tx
|
2024-05-23 10:27:08 +02:00
|
|
|
|
2024-05-24 10:21:19 +02:00
|
|
|
def __connect(self):
|
2024-09-27 08:00:25 +02:00
|
|
|
self.__connection = psycopg2.connect(
|
|
|
|
self.__connection_string, application_name="sl-event-listen"
|
|
|
|
)
|
2024-05-24 10:21:19 +02:00
|
|
|
|
|
|
|
from app.db import Session
|
|
|
|
|
|
|
|
Session.close()
|
|
|
|
|
2024-05-23 10:27:08 +02:00
|
|
|
|
|
|
|
class DeadLetterEventSource(EventSource):
|
2024-07-23 16:11:16 +02:00
|
|
|
def __init__(self, max_retries: int):
|
|
|
|
self.__max_retries = max_retries
|
|
|
|
|
2024-10-17 11:16:33 +02:00
|
|
|
def execute_loop(
|
|
|
|
self, on_event: Callable[[SyncEvent], NoReturn]
|
|
|
|
) -> list[SyncEvent]:
|
|
|
|
threshold = arrow.utcnow().shift(minutes=-_DEAD_LETTER_THRESHOLD_MINUTES)
|
|
|
|
events = SyncEvent.get_dead_letter(
|
|
|
|
older_than=threshold, max_retries=self.__max_retries
|
|
|
|
)
|
|
|
|
if events:
|
|
|
|
LOG.info(f"Got {len(events)} dead letter events")
|
|
|
|
newrelic.agent.record_custom_metric(
|
|
|
|
"Custom/dead_letter_events_to_process", len(events)
|
|
|
|
)
|
|
|
|
for event in events:
|
|
|
|
if event.mark_as_taken(allow_taken_older_than=threshold):
|
|
|
|
on_event(event)
|
|
|
|
return events
|
|
|
|
|
2024-05-23 10:27:08 +02:00
|
|
|
@newrelic.agent.background_task()
|
|
|
|
def run(self, on_event: Callable[[SyncEvent], NoReturn]):
|
|
|
|
while True:
|
|
|
|
try:
|
2024-10-17 11:16:33 +02:00
|
|
|
events = self.execute_loop(on_event)
|
2024-07-24 16:49:55 +02:00
|
|
|
Session.close() # Ensure that we have a new connection and we don't have a dangling tx with a lock
|
|
|
|
if not events:
|
2024-05-23 10:27:08 +02:00
|
|
|
LOG.debug("No dead letter events")
|
|
|
|
sleep(_DEAD_LETTER_INTERVAL_SECONDS)
|
|
|
|
except Exception as e:
|
|
|
|
LOG.warn(f"Error getting dead letter event: {e}")
|
2024-05-24 10:21:19 +02:00
|
|
|
sleep(_DEAD_LETTER_INTERVAL_SECONDS)
|