4
4
5
5
from bolt .db .models import Model
6
6
7
- from .gid import GlobalID
8
-
9
7
10
8
def load_job (job_class_path , parameters ):
11
9
module_path , class_name = job_class_path .rsplit ("." , 1 )
@@ -21,14 +19,14 @@ def to_json(args, kwargs):
21
19
serialized_args = []
22
20
for arg in args :
23
21
if isinstance (arg , Model ):
24
- serialized_args .append (GlobalID .from_instance (arg ))
22
+ serialized_args .append (ModelInstanceParameter .from_instance (arg ))
25
23
else :
26
24
serialized_args .append (arg )
27
25
28
26
serialized_kwargs = {}
29
27
for key , value in kwargs .items ():
30
28
if isinstance (value , Model ):
31
- serialized_kwargs [key ] = GlobalID .from_instance (value )
29
+ serialized_kwargs [key ] = ModelInstanceParameter .from_instance (value )
32
30
else :
33
31
serialized_kwargs [key ] = value
34
32
@@ -38,21 +36,49 @@ def to_json(args, kwargs):
38
36
def from_json (data ):
39
37
args = []
40
38
for arg in data ["args" ]:
41
- if GlobalID .is_gid (arg ):
42
- args .append (GlobalID .to_instance (arg ))
39
+ if ModelInstanceParameter .is_gid (arg ):
40
+ args .append (ModelInstanceParameter .to_instance (arg ))
43
41
else :
44
42
args .append (arg )
45
43
46
44
kwargs = {}
47
45
for key , value in data ["kwargs" ].items ():
48
- if GlobalID .is_gid (value ):
49
- kwargs [key ] = GlobalID .to_instance (value )
46
+ if ModelInstanceParameter .is_gid (value ):
47
+ kwargs [key ] = ModelInstanceParameter .to_instance (value )
50
48
else :
51
49
kwargs [key ] = value
52
50
53
51
return args , kwargs
54
52
55
53
54
+ class ModelInstanceParameter :
55
+ """
56
+ A string representation of a model instance,
57
+ so we can convert a single parameter (model instance itself)
58
+ into a string that can be serialized and stored in the database.
59
+ """
60
+
61
+ @staticmethod
62
+ def from_instance (instance ):
63
+ return f"gid://{ instance ._meta .package_label } /{ instance ._meta .model_name } /{ instance .pk } "
64
+
65
+ @staticmethod
66
+ def to_instance (s ):
67
+ if not s .startswith ("gid://" ):
68
+ raise ValueError ("Invalid ModelInstanceParameter string" )
69
+ package , model , pk = s [6 :].split ("/" )
70
+ from bolt .packages import packages
71
+
72
+ model = packages .get_model (package , model )
73
+ return model .objects .get (pk = pk )
74
+
75
+ @staticmethod
76
+ def is_gid (x ):
77
+ if not isinstance (x , str ):
78
+ return False
79
+ return x .startswith ("gid://" )
80
+
81
+
56
82
class JobType (type ):
57
83
"""
58
84
Metaclass allows us to capture the original args/kwargs
@@ -68,7 +94,15 @@ def __call__(self, *args, **kwargs):
68
94
69
95
70
96
class Job (metaclass = JobType ):
97
+ def run (self ):
98
+ raise NotImplementedError
99
+
71
100
def run_in_background (self , start_at : datetime .datetime | None = None ):
101
+ from .models import JobRequest
102
+
103
+ if unique_existing := self ._get_existing_unique_job_or_request ():
104
+ return unique_existing
105
+
72
106
try :
73
107
# Try to automatically annotate the source of the job
74
108
caller = inspect .stack ()[1 ]
@@ -78,22 +112,56 @@ def run_in_background(self, start_at: datetime.datetime | None = None):
78
112
79
113
parameters = JobParameters .to_json (self ._init_args , self ._init_kwargs )
80
114
81
- from .models import JobRequest
82
-
83
- priority = self .get_priority ()
84
- retries = self .get_retries ()
85
-
86
115
return JobRequest .objects .create (
87
- job_class = f" { self .__module__ } . { self . __class__ . __name__ } " ,
116
+ job_class = self ._job_class_str () ,
88
117
parameters = parameters ,
89
- priority = priority ,
118
+ priority = self . get_priority () ,
90
119
source = source ,
91
- retries = retries ,
120
+ retries = self . get_retries () ,
92
121
start_at = start_at ,
93
122
)
94
123
95
- def run (self ):
96
- raise NotImplementedError
124
+ def _job_class_str (self ):
125
+ return f"{ self .__module__ } .{ self .__class__ .__name__ } "
126
+
127
+ def _get_existing_unique_job_or_request (self ):
128
+ """
129
+ Find pending or running versions of this job that already exist.
130
+ Note this doesn't include instances that may have failed and are
131
+ not yet queued for retry.
132
+ """
133
+ from .models import Job , JobRequest
134
+
135
+ job_class = self ._job_class_str ()
136
+ unique_key = self .get_unique_key ()
137
+
138
+ if not unique_key :
139
+ return None
140
+
141
+ try :
142
+ return JobRequest .objects .get (
143
+ job_class = job_class ,
144
+ unique_key = unique_key ,
145
+ )
146
+ except JobRequest .DoesNotExist :
147
+ pass
148
+
149
+ try :
150
+ return Job .objects .get (
151
+ job_class = job_class ,
152
+ unique_key = unique_key ,
153
+ )
154
+ except Job .DoesNotExist :
155
+ pass
156
+
157
+ return None
158
+
159
+ def get_unique_key (self ) -> str :
160
+ """
161
+ A unique key to prevent duplicate jobs from being queued.
162
+ Enabled by returning a non-empty string.
163
+ """
164
+ raise ""
97
165
98
166
def get_priority (self ) -> int :
99
167
return 0
0 commit comments