@@ -28,7 +28,7 @@ class JobRequest(models.Model):
28
28
Keep all pending job requests in a single table.
29
29
"""
30
30
31
- created_at = models .DateTimeField (auto_now_add = True )
31
+ created_at = models .DateTimeField (auto_now_add = True , db_index = True )
32
32
uuid = models .UUIDField (default = uuid .uuid4 , editable = False , unique = True )
33
33
34
34
job_class = models .CharField (max_length = 255 , db_index = True )
@@ -52,12 +52,12 @@ class Meta:
52
52
def __str__ (self ):
53
53
return f"{ self .job_class } [{ self .uuid } ]"
54
54
55
- def convert_to_result (self ):
55
+ def convert_to_job (self ):
56
56
"""
57
57
JobRequests are the pending jobs that are waiting to be executed.
58
58
We immediately convert them to JobResults when they are picked up.
59
59
"""
60
- result = JobResult .objects .create (
60
+ result = Job .objects .create (
61
61
job_request_uuid = self .uuid ,
62
62
job_class = self .job_class ,
63
63
parameters = self .parameters ,
@@ -73,13 +73,91 @@ def convert_to_result(self):
73
73
return result
74
74
75
75
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
+ )
79
153
80
- def processing ( self ):
81
- return self .filter ( status = JobResultStatuses . PROCESSING )
154
+ # Delete the Job now
155
+ self .delete ( )
82
156
157
+ return result
158
+
159
+
160
+ class JobResultQuerySet (models .QuerySet ):
83
161
def successful (self ):
84
162
return self .filter (status = JobResultStatuses .SUCCESSFUL )
85
163
@@ -95,18 +173,6 @@ def retried(self):
95
173
| models .Q (retry_attempt__gt = 0 )
96
174
)
97
175
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
-
110
176
def retry_failed_jobs (self ):
111
177
for result in self .filter (
112
178
status__in = [JobResultStatuses .ERRORED , JobResultStatuses .LOST ],
@@ -118,8 +184,6 @@ def retry_failed_jobs(self):
118
184
119
185
120
186
class JobResultStatuses (models .TextChoices ):
121
- UNKNOWN = "" , "Unknown" # The initial state
122
- PROCESSING = "PROCESSING" , "Processing"
123
187
SUCCESSFUL = "SUCCESSFUL" , "Successful"
124
188
ERRORED = "ERRORED" , "Errored" # Threw an error
125
189
LOST = (
@@ -134,15 +198,16 @@ class JobResult(models.Model):
134
198
"""
135
199
136
200
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 )
138
205
started_at = models .DateTimeField (blank = True , null = True , db_index = True )
139
206
ended_at = models .DateTimeField (blank = True , null = True , db_index = True )
140
207
error = models .TextField (blank = True )
141
208
status = models .CharField (
142
209
max_length = 20 ,
143
210
choices = JobResultStatuses .choices ,
144
- blank = True ,
145
- default = JobResultStatuses .UNKNOWN ,
146
211
db_index = True ,
147
212
)
148
213
@@ -161,24 +226,7 @@ class JobResult(models.Model):
161
226
objects = JobResultQuerySet .as_manager ()
162
227
163
228
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" ]
182
230
183
231
def retry_job (self , delay : int | None = None ):
184
232
retry_attempt = self .retry_attempt + 1
0 commit comments