-
Notifications
You must be signed in to change notification settings - Fork 18
/
jobs_dao.py
220 lines (182 loc) · 7.03 KB
/
jobs_dao.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import uuid
from datetime import datetime, timedelta
from typing import Iterable
from flask import current_app
from notifications_utils.letter_timings import (
CANCELLABLE_JOB_LETTER_STATUSES,
letter_can_be_cancelled,
)
from notifications_utils.statsd_decorators import statsd
from sqlalchemy import asc, desc, func
from app import db
from app.dao.dao_utils import transactional
from app.dao.date_util import get_query_date_based_on_retention_period
from app.dao.templates_dao import dao_get_template_by_id
from app.models import (
JOB_STATUS_CANCELLED,
JOB_STATUS_FINISHED,
JOB_STATUS_IN_PROGRESS,
JOB_STATUS_PENDING,
JOB_STATUS_SCHEDULED,
LETTER_TYPE,
NOTIFICATION_CANCELLED,
NOTIFICATION_CREATED,
Job,
Notification,
NotificationHistory,
ServiceDataRetention,
Template,
)
@statsd(namespace="dao")
def dao_get_notification_outcomes_for_job(service_id, job_id):
notification = (
db.session.query(func.count(Notification.status).label("count"), Notification.status)
.filter(Notification.service_id == service_id, Notification.job_id == job_id)
.group_by(Notification.status)
)
notification_history = (
db.session.query(func.count(NotificationHistory.status).label("count"), NotificationHistory.status)
.filter(NotificationHistory.service_id == service_id, NotificationHistory.job_id == job_id)
.group_by(NotificationHistory.status)
)
return notification.union(notification_history).all()
def dao_get_job_by_service_id_and_job_id(service_id, job_id):
return Job.query.filter_by(service_id=service_id, id=job_id).first()
def dao_get_jobs_by_service_id(service_id, limit_days=None, page=1, page_size=50, statuses=None):
query_filter = [
Job.service_id == service_id,
Job.original_file_name != current_app.config["TEST_MESSAGE_FILENAME"],
Job.original_file_name != current_app.config["ONE_OFF_MESSAGE_FILENAME"],
]
if limit_days is not None:
query_filter.append(Job.created_at > get_query_date_based_on_retention_period(limit_days))
if statuses is not None and statuses != [""]:
query_filter.append(Job.job_status.in_(statuses))
return (
Job.query.filter(*query_filter)
.order_by(Job.processing_started.desc(), Job.created_at.desc())
.paginate(page=page, per_page=page_size)
)
def dao_get_job_by_id(job_id) -> Job:
return Job.query.filter_by(id=job_id).one()
def dao_archive_jobs(jobs: Iterable[Job]):
"""
Archive the given jobs.
Args:
jobs (Iterable[Job]): The jobs to archive.
"""
for job in jobs:
job.archived = True
db.session.add(job)
db.session.commit()
def dao_get_in_progress_jobs():
return Job.query.filter(Job.job_status == JOB_STATUS_IN_PROGRESS).all()
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.
"""
jobs = (
Job.query.filter(
Job.job_status == JOB_STATUS_SCHEDULED,
Job.scheduled_for < datetime.utcnow(),
)
.order_by(asc(Job.scheduled_for))
.with_for_update()
.all()
)
for job in jobs:
job.job_status = JOB_STATUS_PENDING
db.session.add_all(jobs)
db.session.commit()
return jobs
def dao_get_future_scheduled_job_by_id_and_service_id(job_id, service_id):
return Job.query.filter(
Job.service_id == service_id,
Job.id == job_id,
Job.job_status == JOB_STATUS_SCHEDULED,
Job.scheduled_for > datetime.utcnow(),
).one()
def dao_create_job(job):
if not job.id:
job.id = uuid.uuid4()
db.session.add(job)
db.session.commit()
def dao_update_job(job):
db.session.add(job)
db.session.commit()
def dao_get_jobs_older_than_data_retention(notification_types, limit=None):
flexible_data_retention = ServiceDataRetention.query.filter(
ServiceDataRetention.notification_type.in_(notification_types)
).all()
jobs = []
today = datetime.utcnow().date()
for f in flexible_data_retention:
end_date = today - timedelta(days=f.days_of_retention)
query = (
Job.query.join(Template)
.filter(
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))
)
if limit:
query = query.limit(limit - len(jobs))
jobs.extend(query.all())
end_date = today - timedelta(days=7)
for notification_type in notification_types:
services_with_data_retention = [x.service_id for x in flexible_data_retention if x.notification_type == notification_type]
query = (
Job.query.join(Template)
.filter(
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))
)
if limit:
query = query.limit(limit - len(jobs))
jobs.extend(query.all())
return jobs
@transactional
def dao_cancel_letter_job(job):
number_of_notifications_cancelled = Notification.query.filter(Notification.job_id == job.id).update(
{
"status": NOTIFICATION_CANCELLED,
"updated_at": datetime.utcnow(),
"billable_units": 0,
}
)
job.job_status = JOB_STATUS_CANCELLED
dao_update_job(job)
return number_of_notifications_cancelled
def can_letter_job_be_cancelled(job):
template = dao_get_template_by_id(job.template_id)
if template.template_type != LETTER_TYPE:
return (
False,
"Only letter jobs can be cancelled through this endpoint. This is not a letter job.",
)
notifications = Notification.query.filter(Notification.job_id == job.id).all()
count_notifications = len(notifications)
if job.job_status != JOB_STATUS_FINISHED or count_notifications != job.notification_count:
return (
False,
"We are still processing these letters, please try again in a minute.",
)
count_cancellable_notifications = len([n for n in notifications if n.status in CANCELLABLE_JOB_LETTER_STATUSES])
if count_cancellable_notifications != job.notification_count or not letter_can_be_cancelled(
NOTIFICATION_CREATED, job.created_at
):
return (
False,
"It’s too late to cancel sending, these letters have already been sent.",
)
return True, None