Skip to content

Commit a50afb9

Browse files
committed
Add Job model to separate active from results
1 parent f937009 commit a50afb9

File tree

6 files changed

+269
-100
lines changed

6 files changed

+269
-100
lines changed

bolt-jobs/bolt/jobs/admin.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from bolt.admin.dates import DatetimeRangeAliases
99
from bolt.http import HttpResponseRedirect
1010

11-
from .models import JobRequest, JobResult
11+
from .models import Job, JobRequest, JobResult
1212

1313

1414
class SuccessfulJobsCard(Card):
@@ -82,6 +82,22 @@ class DetailView(AdminModelDetailView):
8282
model = JobRequest
8383

8484

85+
@register_viewset
86+
class JobViewset(AdminModelViewset):
87+
class ListView(AdminModelListView):
88+
nav_section = "Jobs"
89+
model = Job
90+
fields = ["id", "job_class", "priority", "created_at", "started_at"]
91+
actions = ["Delete"]
92+
93+
def perform_action(self, action: str, target_pks: list):
94+
if action == "Delete":
95+
Job.objects.filter(pk__in=target_pks).delete()
96+
97+
class DetailView(AdminModelDetailView):
98+
model = Job
99+
100+
85101
@register_viewset
86102
class JobResultViewset(AdminModelViewset):
87103
class ListView(AdminModelListView):
@@ -91,7 +107,7 @@ class ListView(AdminModelListView):
91107
"id",
92108
"job_class",
93109
"priority",
94-
"started_at",
110+
"created_at",
95111
"status",
96112
]
97113
cards = [
@@ -102,11 +118,9 @@ class ListView(AdminModelListView):
102118
]
103119
filters = [
104120
"Successful",
105-
"Processing",
106121
"Errored",
107122
"Lost",
108123
"Retried",
109-
"Unknown",
110124
]
111125
actions = [
112126
"Retry",
@@ -120,14 +134,10 @@ def get_initial_queryset(self):
120134
return queryset.successful()
121135
if self.filter == "Errored":
122136
return queryset.errored()
123-
if self.filter == "Processing":
124-
return queryset.processing()
125137
if self.filter == "Lost":
126138
return queryset.lost()
127139
if self.filter == "Retried":
128140
return queryset.retried()
129-
if self.filter == "Unknown":
130-
return queryset.unknown()
131141
return queryset
132142

133143
def get_fields(self):

bolt-jobs/bolt/jobs/cli.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from bolt.utils import timezone
77

8-
from .models import JobRequest, JobResult
8+
from .models import Job, JobRequest, JobResult
99
from .workers import Worker
1010

1111
logger = logging.getLogger("bolt.jobs")
@@ -62,12 +62,14 @@ def clear_completed(older_than):
6262
@cli.command()
6363
def stats():
6464
pending = JobRequest.objects.count()
65+
processing = Job.objects.count()
6566

66-
processing = JobResult.objects.processing().count()
6767
successful = JobResult.objects.successful().count()
6868
errored = JobResult.objects.errored().count()
69+
lost = JobResult.objects.lost().count()
6970

70-
click.echo(f"Pending: {click.style(pending, bold=True)}")
71-
click.echo(f"Processing: {click.style(processing, bold=True)}")
72-
click.echo(f"Successful: {click.style(successful, bold=True)}")
73-
click.echo(f"Errored: {click.style(errored, bold=True)}")
71+
click.secho(f"Pending: {pending}", bold=True)
72+
click.secho(f"Processing: {processing}", bold=True)
73+
click.secho(f"Successful: {successful}", bold=True, fg="green")
74+
click.secho(f"Errored: {errored}", bold=True, fg="red")
75+
click.secho(f"Lost: {lost}", bold=True, fg="yellow")
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Generated by Bolt 5.0.dev20240114170303 on 2024-01-17 18:45
2+
3+
import uuid
4+
5+
from bolt.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("boltqueue", "0011_jobrequest_retries_jobrequest_retry_attempt_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Job",
16+
fields=[
17+
(
18+
"id",
19+
models.BigAutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
(
27+
"uuid",
28+
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
29+
),
30+
("created_at", models.DateTimeField(auto_now_add=True)),
31+
(
32+
"started_at",
33+
models.DateTimeField(blank=True, db_index=True, null=True),
34+
),
35+
("job_request_uuid", models.UUIDField(db_index=True)),
36+
("job_class", models.CharField(db_index=True, max_length=255)),
37+
("parameters", models.JSONField(blank=True, null=True)),
38+
("priority", models.IntegerField(db_index=True, default=0)),
39+
("source", models.TextField(blank=True)),
40+
("retries", models.IntegerField(default=0)),
41+
("retry_attempt", models.IntegerField(default=0)),
42+
],
43+
),
44+
migrations.AddField(
45+
model_name="jobresult",
46+
name="job_uuid",
47+
field=models.UUIDField(db_index=True, default=uuid.uuid4),
48+
preserve_default=False,
49+
),
50+
migrations.AlterField(
51+
model_name="jobresult",
52+
name="status",
53+
field=models.CharField(
54+
choices=[
55+
("SUCCESSFUL", "Successful"),
56+
("ERRORED", "Errored"),
57+
("LOST", "Lost"),
58+
],
59+
db_index=True,
60+
max_length=20,
61+
),
62+
),
63+
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Bolt 5.0.dev20240114170303 on 2024-01-17 19:24
2+
3+
from bolt.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("boltqueue", "0012_job_jobresult_job_uuid_alter_jobresult_status"),
9+
]
10+
11+
operations = [
12+
migrations.AlterModelOptions(
13+
name="job",
14+
options={"ordering": ["-created_at"]},
15+
),
16+
migrations.AlterModelOptions(
17+
name="jobresult",
18+
options={"ordering": ["-created_at"]},
19+
),
20+
migrations.AlterField(
21+
model_name="job",
22+
name="created_at",
23+
field=models.DateTimeField(auto_now_add=True, db_index=True),
24+
),
25+
migrations.AlterField(
26+
model_name="jobrequest",
27+
name="created_at",
28+
field=models.DateTimeField(auto_now_add=True, db_index=True),
29+
),
30+
migrations.AlterField(
31+
model_name="jobresult",
32+
name="created_at",
33+
field=models.DateTimeField(auto_now_add=True, db_index=True),
34+
),
35+
]

bolt-jobs/bolt/jobs/models.py

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class JobRequest(models.Model):
2828
Keep all pending job requests in a single table.
2929
"""
3030

31-
created_at = models.DateTimeField(auto_now_add=True)
31+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
3232
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
3333

3434
job_class = models.CharField(max_length=255, db_index=True)
@@ -52,12 +52,12 @@ class Meta:
5252
def __str__(self):
5353
return f"{self.job_class} [{self.uuid}]"
5454

55-
def convert_to_result(self):
55+
def convert_to_job(self):
5656
"""
5757
JobRequests are the pending jobs that are waiting to be executed.
5858
We immediately convert them to JobResults when they are picked up.
5959
"""
60-
result = JobResult.objects.create(
60+
result = Job.objects.create(
6161
job_request_uuid=self.uuid,
6262
job_class=self.job_class,
6363
parameters=self.parameters,
@@ -73,13 +73,91 @@ def convert_to_result(self):
7373
return result
7474

7575

76-
class JobResultQuerySet(models.QuerySet):
77-
def unknown(self):
78-
return self.filter(status=JobResultStatuses.UNKNOWN)
76+
class JobQuerySet(models.QuerySet):
77+
def mark_lost_jobs(self):
78+
# Nothing should be pending after more than a 24 hrs... consider it lost
79+
# Downside to these is that they are mark lost pretty late?
80+
# In theory we could save a timeout per-job and mark them timed-out more quickly,
81+
# but if they're still running, we can't actually send a signal to cancel it...
82+
now = timezone.now()
83+
one_day_ago = now - datetime.timedelta(days=1)
84+
lost_jobs = self.filter(created_at__lt=one_day_ago)
85+
for job in lost_jobs:
86+
job.convert_to_result(
87+
ended_at=now,
88+
error="",
89+
status=JobResultStatuses.LOST,
90+
)
91+
92+
93+
class Job(models.Model):
94+
"""
95+
All active jobs are stored in this table.
96+
"""
97+
98+
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
99+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
100+
started_at = models.DateTimeField(blank=True, null=True, db_index=True)
101+
102+
# From the JobRequest
103+
job_request_uuid = models.UUIDField(db_index=True)
104+
job_class = models.CharField(max_length=255, db_index=True)
105+
parameters = models.JSONField(blank=True, null=True)
106+
priority = models.IntegerField(default=0, db_index=True)
107+
source = models.TextField(blank=True)
108+
retries = models.IntegerField(default=0)
109+
retry_attempt = models.IntegerField(default=0)
110+
111+
objects = JobQuerySet.as_manager()
112+
113+
class Meta:
114+
ordering = ["-created_at"]
115+
116+
def run(self):
117+
# This is how we know it has been picked up
118+
self.started_at = timezone.now()
119+
self.save(update_fields=["started_at"])
120+
121+
try:
122+
job = load_job(self.job_class, self.parameters)
123+
job.run()
124+
status = JobResultStatuses.SUCCESSFUL
125+
error = ""
126+
except Exception as e:
127+
status = JobResultStatuses.ERRORED
128+
error = "".join(traceback.format_tb(e.__traceback__))
129+
logger.exception(e)
130+
131+
return self.convert_to_result(status=status, error=error)
132+
133+
def convert_to_result(self, *, status, error=""):
134+
"""
135+
Convert this Job to a JobResult.
136+
"""
137+
result = JobResult.objects.create(
138+
ended_at=timezone.now(),
139+
error=error,
140+
status=status,
141+
# From the Job
142+
job_uuid=self.uuid,
143+
started_at=self.started_at,
144+
# From the JobRequest
145+
job_request_uuid=self.job_request_uuid,
146+
job_class=self.job_class,
147+
parameters=self.parameters,
148+
priority=self.priority,
149+
source=self.source,
150+
retries=self.retries,
151+
retry_attempt=self.retry_attempt,
152+
)
79153

80-
def processing(self):
81-
return self.filter(status=JobResultStatuses.PROCESSING)
154+
# Delete the Job now
155+
self.delete()
82156

157+
return result
158+
159+
160+
class JobResultQuerySet(models.QuerySet):
83161
def successful(self):
84162
return self.filter(status=JobResultStatuses.SUCCESSFUL)
85163

@@ -95,18 +173,6 @@ def retried(self):
95173
| models.Q(retry_attempt__gt=0)
96174
)
97175

98-
def mark_lost_jobs(self):
99-
# Nothing should be pending after more than a 24 hrs... consider it lost
100-
# Downside to these is that they are mark lost pretty late?
101-
# In theory we could save a timeout per-job and mark them timed-out more quickly,
102-
# but if they're still running, we can't actually send a signal to cancel it...
103-
now = timezone.now()
104-
one_day_ago = now - datetime.timedelta(days=1)
105-
self.filter(
106-
status__in=[JobResultStatuses.PROCESSING, JobResultStatuses.UNKNOWN],
107-
created_at__lt=one_day_ago,
108-
).update(status=JobResultStatuses.LOST, ended_at=now)
109-
110176
def retry_failed_jobs(self):
111177
for result in self.filter(
112178
status__in=[JobResultStatuses.ERRORED, JobResultStatuses.LOST],
@@ -118,8 +184,6 @@ def retry_failed_jobs(self):
118184

119185

120186
class JobResultStatuses(models.TextChoices):
121-
UNKNOWN = "", "Unknown" # The initial state
122-
PROCESSING = "PROCESSING", "Processing"
123187
SUCCESSFUL = "SUCCESSFUL", "Successful"
124188
ERRORED = "ERRORED", "Errored" # Threw an error
125189
LOST = (
@@ -134,15 +198,16 @@ class JobResult(models.Model):
134198
"""
135199

136200
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
137-
created_at = models.DateTimeField(auto_now_add=True)
201+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
202+
203+
# From the Job
204+
job_uuid = models.UUIDField(db_index=True)
138205
started_at = models.DateTimeField(blank=True, null=True, db_index=True)
139206
ended_at = models.DateTimeField(blank=True, null=True, db_index=True)
140207
error = models.TextField(blank=True)
141208
status = models.CharField(
142209
max_length=20,
143210
choices=JobResultStatuses.choices,
144-
blank=True,
145-
default=JobResultStatuses.UNKNOWN,
146211
db_index=True,
147212
)
148213

@@ -161,24 +226,7 @@ class JobResult(models.Model):
161226
objects = JobResultQuerySet.as_manager()
162227

163228
class Meta:
164-
ordering = ["-started_at"]
165-
166-
def process_job(self):
167-
self.started_at = timezone.now()
168-
self.status = JobResultStatuses.PROCESSING
169-
self.save(update_fields=["started_at", "status"])
170-
171-
try:
172-
job = load_job(self.job_class, self.parameters)
173-
job.run()
174-
self.status = JobResultStatuses.SUCCESSFUL
175-
except Exception as e:
176-
self.error = "".join(traceback.format_tb(e.__traceback__))
177-
self.status = JobResultStatuses.ERRORED
178-
logger.exception(e)
179-
180-
self.ended_at = timezone.now()
181-
self.save(update_fields=["ended_at", "error", "status"])
229+
ordering = ["-created_at"]
182230

183231
def retry_job(self, delay: int | None = None):
184232
retry_attempt = self.retry_attempt + 1

0 commit comments

Comments
 (0)