2024-08-20 12:33:04 -07:00
|
|
|
import os
|
2017-07-27 11:10:22 +01:00
|
|
|
import uuid
|
2024-05-23 13:59:51 -07:00
|
|
|
from datetime import timedelta
|
2016-09-21 14:35:23 +01:00
|
|
|
|
2016-10-11 14:30:40 +01:00
|
|
|
from flask import current_app
|
2024-12-30 12:11:19 -08:00
|
|
|
from sqlalchemy import and_, asc, desc, func, select, update
|
2016-09-21 14:35:23 +01:00
|
|
|
|
2016-01-15 11:12:05 +00:00
|
|
|
from app import db
|
2024-10-16 11:30:36 -07:00
|
|
|
from app.dao.pagination import Pagination
|
2024-01-15 14:22:56 -05:00
|
|
|
from app.enums import JobStatus
|
2017-06-06 16:01:27 +01:00
|
|
|
from app.models import (
|
2021-03-10 13:55:06 +00:00
|
|
|
FactNotificationStatus,
|
|
|
|
|
Job,
|
2018-12-12 12:57:33 +00:00
|
|
|
Notification,
|
2019-06-10 17:40:28 +01:00
|
|
|
ServiceDataRetention,
|
2021-03-10 13:55:06 +00:00
|
|
|
Template,
|
2017-06-06 16:01:27 +01:00
|
|
|
)
|
2024-08-20 11:46:58 -07:00
|
|
|
from app.utils import midnight_n_days_ago, utc_now
|
2016-08-23 16:46:58 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def dao_get_notification_outcomes_for_job(service_id, job_id):
|
2024-10-15 13:26:13 -07:00
|
|
|
stmt = (
|
|
|
|
|
select(func.count(Notification.status).label("count"), Notification.status)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(Notification.service_id == service_id, Notification.job_id == job_id)
|
2023-08-29 14:54:30 -07:00
|
|
|
.group_by(Notification.status)
|
|
|
|
|
)
|
2024-10-15 13:26:13 -07:00
|
|
|
notification_statuses = db.session.execute(stmt).all()
|
2017-03-15 15:26:58 +00:00
|
|
|
|
2019-12-30 16:17:00 +00:00
|
|
|
if not notification_statuses:
|
2024-10-15 13:26:13 -07:00
|
|
|
stmt = select(
|
|
|
|
|
FactNotificationStatus.notification_count.label("count"),
|
|
|
|
|
FactNotificationStatus.notification_status.label("status"),
|
2024-12-20 08:09:19 -08:00
|
|
|
).where(
|
2024-10-15 13:26:13 -07:00
|
|
|
FactNotificationStatus.service_id == service_id,
|
|
|
|
|
FactNotificationStatus.job_id == job_id,
|
2023-08-29 14:54:30 -07:00
|
|
|
)
|
2024-10-15 13:26:13 -07:00
|
|
|
notification_statuses = db.session.execute(stmt).all()
|
2019-12-30 16:17:00 +00:00
|
|
|
return notification_statuses
|
|
|
|
|
|
2017-03-15 15:26:58 +00:00
|
|
|
|
2016-02-24 17:12:30 +00:00
|
|
|
def dao_get_job_by_service_id_and_job_id(service_id, job_id):
|
2024-12-19 11:10:03 -08:00
|
|
|
stmt = select(Job).where(Job.service_id == service_id, Job.id == job_id)
|
2024-10-15 13:26:13 -07:00
|
|
|
return db.session.execute(stmt).scalars().one()
|
2016-01-15 11:12:05 +00:00
|
|
|
|
|
|
|
|
|
2023-05-23 08:31:30 -07:00
|
|
|
def dao_get_unfinished_jobs():
|
2025-01-14 12:01:46 -08:00
|
|
|
|
2024-10-15 13:26:13 -07:00
|
|
|
stmt = select(Job).filter(Job.processing_finished.is_(None))
|
2025-01-08 08:44:49 -08:00
|
|
|
return db.session.execute(stmt).scalars().all()
|
2023-05-23 08:31:30 -07:00
|
|
|
|
|
|
|
|
|
2020-05-12 10:47:14 +01:00
|
|
|
def dao_get_jobs_by_service_id(
|
|
|
|
|
service_id,
|
|
|
|
|
*,
|
|
|
|
|
limit_days=None,
|
2025-08-07 18:35:58 -07:00
|
|
|
use_processing_time=False,
|
2020-05-12 10:47:14 +01:00
|
|
|
page=1,
|
|
|
|
|
page_size=50,
|
|
|
|
|
statuses=None,
|
|
|
|
|
):
|
2016-10-11 14:30:40 +01:00
|
|
|
query_filter = [
|
|
|
|
|
Job.service_id == service_id,
|
2023-08-29 14:54:30 -07:00
|
|
|
Job.original_file_name != current_app.config["TEST_MESSAGE_FILENAME"],
|
|
|
|
|
Job.original_file_name != current_app.config["ONE_OFF_MESSAGE_FILENAME"],
|
2016-10-11 14:30:40 +01:00
|
|
|
]
|
2016-05-24 17:21:04 +01:00
|
|
|
if limit_days is not None:
|
2025-08-07 18:35:58 -07:00
|
|
|
if use_processing_time:
|
|
|
|
|
query_filter.append(
|
2025-08-25 16:28:54 -04:00
|
|
|
func.coalesce(Job.processing_started, Job.created_at)
|
|
|
|
|
>= midnight_n_days_ago(limit_days)
|
2025-08-07 18:35:58 -07:00
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
query_filter.append(Job.created_at >= midnight_n_days_ago(limit_days))
|
2023-08-29 14:54:30 -07:00
|
|
|
if statuses is not None and statuses != [""]:
|
|
|
|
|
query_filter.append(Job.job_status.in_(statuses))
|
2024-10-15 13:26:13 -07:00
|
|
|
|
2024-10-16 11:30:36 -07:00
|
|
|
total_items = db.session.execute(
|
2024-12-20 08:09:19 -08:00
|
|
|
select(func.count()).select_from(Job).where(*query_filter)
|
2024-10-16 11:30:36 -07:00
|
|
|
).scalar_one()
|
2024-10-16 10:16:04 -07:00
|
|
|
|
2024-10-16 11:30:36 -07:00
|
|
|
offset = (page - 1) * page_size
|
|
|
|
|
stmt = (
|
|
|
|
|
select(Job)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(*query_filter)
|
2025-06-26 17:49:17 -07:00
|
|
|
.order_by(
|
|
|
|
|
func.coalesce(Job.processing_started, Job.created_at).desc(), Job.id.desc()
|
|
|
|
|
)
|
2024-10-16 11:30:36 -07:00
|
|
|
.limit(page_size)
|
|
|
|
|
.offset(offset)
|
2024-10-15 14:31:27 -07:00
|
|
|
)
|
2024-10-16 11:30:36 -07:00
|
|
|
items = db.session.execute(stmt).scalars().all()
|
|
|
|
|
return Pagination(items, page, page_size, total_items)
|
|
|
|
|
|
2016-01-15 11:12:05 +00:00
|
|
|
|
2020-09-28 09:57:32 +01:00
|
|
|
def dao_get_scheduled_job_stats(
|
|
|
|
|
service_id,
|
|
|
|
|
):
|
2024-10-16 10:48:17 -07:00
|
|
|
|
2024-10-15 13:26:13 -07:00
|
|
|
stmt = select(
|
|
|
|
|
func.count(Job.id),
|
|
|
|
|
func.min(Job.scheduled_for),
|
2024-12-20 08:09:19 -08:00
|
|
|
).where(
|
2024-10-15 13:26:13 -07:00
|
|
|
Job.service_id == service_id,
|
|
|
|
|
Job.job_status == JobStatus.SCHEDULED,
|
2023-08-29 14:54:30 -07:00
|
|
|
)
|
2024-10-16 10:56:08 -07:00
|
|
|
return db.session.execute(stmt).one()
|
2020-09-28 09:57:32 +01:00
|
|
|
|
|
|
|
|
|
2016-02-24 17:12:30 +00:00
|
|
|
def dao_get_job_by_id(job_id):
|
2024-12-19 11:10:03 -08:00
|
|
|
stmt = select(Job).where(Job.id == job_id)
|
2024-10-15 13:26:13 -07:00
|
|
|
return db.session.execute(stmt).scalars().one()
|
2018-11-22 16:47:07 +00:00
|
|
|
|
2018-11-22 17:49:04 +00:00
|
|
|
|
2018-11-22 16:47:07 +00:00
|
|
|
def dao_archive_job(job):
|
|
|
|
|
job.archived = True
|
|
|
|
|
db.session.add(job)
|
|
|
|
|
db.session.commit()
|
2016-02-24 17:12:30 +00:00
|
|
|
|
|
|
|
|
|
2016-10-07 12:28:42 +01:00
|
|
|
def dao_set_scheduled_jobs_to_pending():
|
|
|
|
|
"""
|
|
|
|
|
Sets all past scheduled jobs to pending, and then returns them for further processing.
|
|
|
|
|
|
|
|
|
|
this is used in the run_scheduled_jobs task, so we put a FOR UPDATE lock on the job table for the duration of
|
|
|
|
|
the transaction so that if the task is run more than once concurrently, one task will block the other select
|
|
|
|
|
from completing until it commits.
|
|
|
|
|
"""
|
2024-10-16 11:49:30 -07:00
|
|
|
stmt = (
|
2024-10-16 12:22:00 -07:00
|
|
|
select(Job)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(
|
2024-01-15 14:22:56 -05:00
|
|
|
Job.job_status == JobStatus.SCHEDULED,
|
2024-05-23 13:59:51 -07:00
|
|
|
Job.scheduled_for < utc_now(),
|
2024-10-16 12:22:00 -07:00
|
|
|
)
|
2023-08-29 14:54:30 -07:00
|
|
|
.order_by(asc(Job.scheduled_for))
|
|
|
|
|
.with_for_update()
|
|
|
|
|
)
|
2024-10-16 12:05:56 -07:00
|
|
|
jobs = db.session.execute(stmt).scalars().all()
|
2016-08-24 16:24:30 +01:00
|
|
|
|
2016-10-07 12:28:42 +01:00
|
|
|
for job in jobs:
|
2024-01-15 14:22:56 -05:00
|
|
|
job.job_status = JobStatus.PENDING
|
2016-10-07 12:28:42 +01:00
|
|
|
|
|
|
|
|
db.session.add_all(jobs)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
return jobs
|
|
|
|
|
|
|
|
|
|
|
2016-09-01 14:31:01 +01:00
|
|
|
def dao_get_future_scheduled_job_by_id_and_service_id(job_id, service_id):
|
2024-12-20 08:09:19 -08:00
|
|
|
stmt = select(Job).where(
|
2023-08-29 14:54:30 -07:00
|
|
|
Job.service_id == service_id,
|
|
|
|
|
Job.id == job_id,
|
2024-01-15 14:22:56 -05:00
|
|
|
Job.job_status == JobStatus.SCHEDULED,
|
2024-05-23 13:59:51 -07:00
|
|
|
Job.scheduled_for > utc_now(),
|
2024-10-16 11:49:30 -07:00
|
|
|
)
|
|
|
|
|
return db.session.execute(stmt).scalars().one()
|
2016-09-01 14:31:01 +01:00
|
|
|
|
|
|
|
|
|
2016-02-24 17:12:30 +00:00
|
|
|
def dao_create_job(job):
|
2017-07-27 11:10:22 +01:00
|
|
|
if not job.id:
|
|
|
|
|
job.id = uuid.uuid4()
|
2016-02-24 17:12:30 +00:00
|
|
|
db.session.add(job)
|
|
|
|
|
db.session.commit()
|
2024-08-20 11:19:43 -07:00
|
|
|
# We are seeing weird time anomalies where a job can be created on
|
|
|
|
|
# 8/19 yet show a created_at time of 8/16. This seems to be the only
|
|
|
|
|
# place the created_at value is set so do some double-checking and debugging
|
|
|
|
|
orig_time = job.created_at
|
|
|
|
|
now_time = utc_now()
|
|
|
|
|
diff_time = now_time - orig_time
|
2025-06-25 11:56:27 -07:00
|
|
|
current_app.logger.warning(
|
2025-02-27 13:24:32 -08:00
|
|
|
f"#notify-debug-admin-1859 dao_create_job orig created at {orig_time} and now {now_time}"
|
2024-08-20 11:19:43 -07:00
|
|
|
)
|
2024-08-20 12:33:04 -07:00
|
|
|
if diff_time.total_seconds() > 300: # It should be only a few seconds diff at most
|
2025-06-25 11:56:27 -07:00
|
|
|
current_app.logger.warning(
|
2025-02-27 13:24:32 -08:00
|
|
|
"#notify-debug-admin-1859 Something is wrong with job.created_at!"
|
2024-08-20 11:19:43 -07:00
|
|
|
)
|
2024-08-20 12:33:04 -07:00
|
|
|
if os.getenv("NOTIFY_ENVIRONMENT") not in ["test"]:
|
|
|
|
|
job.created_at = now_time
|
|
|
|
|
dao_update_job(job)
|
2025-06-25 11:56:27 -07:00
|
|
|
current_app.logger.warning(
|
2025-02-27 13:24:32 -08:00
|
|
|
f"#notify-debug-admin-1859 Job created_at reset to {job.created_at}"
|
2024-08-20 12:33:04 -07:00
|
|
|
)
|
2016-02-24 17:12:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def dao_update_job(job):
|
|
|
|
|
db.session.add(job)
|
|
|
|
|
db.session.commit()
|
2024-12-30 11:45:14 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def dao_update_job_status_to_error(job):
|
2024-12-30 12:11:19 -08:00
|
|
|
stmt = update(Job).where(Job.id == job.id).values(job_status=JobStatus.ERROR)
|
|
|
|
|
db.session.execute(stmt)
|
2024-12-30 11:45:14 -08:00
|
|
|
db.session.commit()
|
2016-09-07 15:36:07 +01:00
|
|
|
|
|
|
|
|
|
2018-11-19 17:09:27 +00:00
|
|
|
def dao_get_jobs_older_than_data_retention(notification_types):
|
2024-12-20 08:09:19 -08:00
|
|
|
stmt = select(ServiceDataRetention).where(
|
2018-11-19 17:09:27 +00:00
|
|
|
ServiceDataRetention.notification_type.in_(notification_types)
|
2024-10-17 08:56:15 -07:00
|
|
|
)
|
2024-10-17 09:15:21 -07:00
|
|
|
flexible_data_retention = db.session.execute(stmt).scalars().all()
|
2018-11-19 17:09:27 +00:00
|
|
|
jobs = []
|
2024-05-23 13:59:51 -07:00
|
|
|
today = utc_now().date()
|
2018-11-19 17:09:27 +00:00
|
|
|
for f in flexible_data_retention:
|
|
|
|
|
end_date = today - timedelta(days=f.days_of_retention)
|
2024-10-17 08:56:15 -07:00
|
|
|
stmt = (
|
|
|
|
|
select(Job)
|
|
|
|
|
.join(Template)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(
|
2023-08-29 14:54:30 -07:00
|
|
|
func.coalesce(Job.scheduled_for, Job.created_at) < end_date,
|
|
|
|
|
Job.archived == False, # noqa
|
|
|
|
|
Template.template_type == f.notification_type,
|
|
|
|
|
Job.service_id == f.service_id,
|
|
|
|
|
)
|
|
|
|
|
.order_by(desc(Job.created_at))
|
|
|
|
|
)
|
2024-10-17 09:05:33 -07:00
|
|
|
jobs.extend(db.session.execute(stmt).scalars().all())
|
2018-11-19 17:09:27 +00:00
|
|
|
|
2024-08-21 10:59:49 -07:00
|
|
|
# notify-api-1287, make default data retention 7 days, 23 hours
|
2024-08-21 10:46:13 -07:00
|
|
|
end_date = today - timedelta(days=7, hours=23)
|
2018-11-19 17:09:27 +00:00
|
|
|
for notification_type in notification_types:
|
|
|
|
|
services_with_data_retention = [
|
2023-08-29 14:54:30 -07:00
|
|
|
x.service_id
|
|
|
|
|
for x in flexible_data_retention
|
|
|
|
|
if x.notification_type == notification_type
|
2018-11-19 17:09:27 +00:00
|
|
|
]
|
2024-10-17 08:56:15 -07:00
|
|
|
stmt = (
|
|
|
|
|
select(Job)
|
|
|
|
|
.join(Template)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(
|
2023-08-29 14:54:30 -07:00
|
|
|
func.coalesce(Job.scheduled_for, Job.created_at) < end_date,
|
|
|
|
|
Job.archived == False, # noqa
|
|
|
|
|
Template.template_type == notification_type,
|
|
|
|
|
Job.service_id.notin_(services_with_data_retention),
|
|
|
|
|
)
|
|
|
|
|
.order_by(desc(Job.created_at))
|
|
|
|
|
)
|
2024-10-17 09:05:33 -07:00
|
|
|
jobs.extend(db.session.execute(stmt).scalars().all())
|
2017-06-06 16:01:27 +01:00
|
|
|
|
2018-11-19 17:09:27 +00:00
|
|
|
return jobs
|
2017-04-07 11:50:56 +01:00
|
|
|
|
|
|
|
|
|
2019-11-05 15:14:23 +00:00
|
|
|
def find_jobs_with_missing_rows():
|
2020-09-26 11:06:44 +01:00
|
|
|
# Jobs can be a maximum of 100,000 rows. It typically takes 10 minutes to create all those notifications.
|
|
|
|
|
# Using 20 minutes as a condition seems reasonable.
|
2024-05-23 13:59:51 -07:00
|
|
|
ten_minutes_ago = utc_now() - timedelta(minutes=20)
|
|
|
|
|
yesterday = utc_now() - timedelta(days=1)
|
2023-08-29 14:54:30 -07:00
|
|
|
jobs_with_rows_missing = (
|
2024-10-17 07:36:24 -07:00
|
|
|
select(Job)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(
|
2024-01-15 14:22:56 -05:00
|
|
|
Job.job_status == JobStatus.FINISHED,
|
2023-08-29 14:54:30 -07:00
|
|
|
Job.processing_finished < ten_minutes_ago,
|
|
|
|
|
Job.processing_finished > yesterday,
|
|
|
|
|
Job.id == Notification.job_id,
|
|
|
|
|
)
|
|
|
|
|
.group_by(Job)
|
|
|
|
|
.having(func.count(Notification.id) != Job.notification_count)
|
2019-11-05 15:14:23 +00:00
|
|
|
)
|
|
|
|
|
|
2024-10-17 08:22:34 -07:00
|
|
|
return db.session.execute(jobs_with_rows_missing).scalars().all()
|
2019-11-05 15:14:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_missing_row_for_job(job_id, job_size):
|
2024-10-17 07:36:24 -07:00
|
|
|
expected_row_numbers = select(
|
2023-08-29 14:54:30 -07:00
|
|
|
func.generate_series(0, job_size - 1).label("row")
|
2019-11-05 15:14:23 +00:00
|
|
|
).subquery()
|
|
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
query = (
|
2024-10-17 07:36:24 -07:00
|
|
|
select(
|
2023-08-29 14:54:30 -07:00
|
|
|
Notification.job_row_number, expected_row_numbers.c.row.label("missing_row")
|
|
|
|
|
)
|
|
|
|
|
.outerjoin(
|
|
|
|
|
Notification,
|
|
|
|
|
and_(
|
|
|
|
|
expected_row_numbers.c.row == Notification.job_row_number,
|
|
|
|
|
Notification.job_id == job_id,
|
|
|
|
|
),
|
|
|
|
|
)
|
2024-12-20 08:09:19 -08:00
|
|
|
.where(Notification.job_row_number == None) # noqa
|
2019-11-05 15:14:23 +00:00
|
|
|
)
|
2024-10-17 08:44:37 -07:00
|
|
|
return db.session.execute(query).all()
|