@@ -2,10 +2,11 @@ package com.lucidchart.piezo
22
33import java .sql .{ResultSet , Timestamp }
44import java .util .Date
5- import org .quartz .{JobKey , TriggerKey }
5+ import org .quartz .{JobDataMap , JobKey , TriggerKey }
66import org .slf4j .LoggerFactory
77import org .slf4j .Logger
88import java .sql .Connection
9+ import java .time .Instant
910
1011case class JobRecord (
1112 name : String ,
@@ -21,6 +22,18 @@ case class JobRecord(
2122class JobHistoryModel (getConnection : () => Connection ) {
2223 val logger : Logger = LoggerFactory .getLogger(this .getClass)
2324
25+ // Trigger Group for records that aren't deletable
26+ private final val oneTimeJobTriggerGroup = " ONE_TIME_JOB"
27+ final def oneTimeTriggerKey (fireInstanceId : Long ): TriggerKey =
28+ TriggerKey (fireInstanceId.toString, oneTimeJobTriggerGroup)
29+
30+ // Methods to store the one-time-job id in a job-data-map
31+ final val jobDataMapOneTimeJobKey = " OneTimeJobId"
32+ final def getOneTimeJobIdFromDataMap (jobDataMap : JobDataMap ) = jobDataMap.getString(jobDataMapOneTimeJobKey)
33+ final def createJobDataMapForOneTimeJob (id : Long ): JobDataMap = new JobDataMap (
34+ java.util.Map .of(jobDataMapOneTimeJobKey, id),
35+ )
36+
2437 def addJob (
2538 fireInstanceId : String ,
2639 jobKey : JobKey ,
@@ -66,10 +79,11 @@ class JobHistoryModel(getConnection: () => Connection) {
6679 val connection = getConnection()
6780 try {
6881 val prepared = connection.prepareStatement(
69- """
82+ s """
7083 DELETE
7184 FROM job_history
7285 WHERE start < ?
86+ AND trigger_group != $oneTimeJobTriggerGroup
7387 """ .stripMargin,
7488 )
7589 prepared.setTimestamp(1 , new Timestamp (minStart))
@@ -190,4 +204,99 @@ class JobHistoryModel(getConnection: () => Connection) {
190204 rs.getString(" fire_instance_id" ),
191205 )
192206 }
207+
208+ /**
209+ * Check if we have already triggered a one-time-job with the given trigger key and fireInstanceId.
210+ *
211+ * This is useful for seeing if a one-time job has already been triggered, to ensure that triggering a one-time job
212+ * with the same instance id is an idempotent operation. If the one-time job has not been triggered, the same
213+ * transaction is used to add the one-time-job to the database, to avoid race conditions
214+ */
215+ def addOneTimeJobIfNotExists (jobKey : JobKey , fireInstanceId : Long ): Boolean = {
216+ val connection = getConnection()
217+ connection.setAutoCommit(false )
218+
219+ // Use a trigger key that the database won't clean up in "JobHistoryCleanup"
220+ val triggerKey : TriggerKey = oneTimeTriggerKey(fireInstanceId)
221+
222+ try {
223+ val prepared = connection.prepareStatement(
224+ """
225+ SELECT EXISTS(
226+ SELECT 1
227+ FROM job_history
228+ WHERE fire_instance_id=?
229+ LIMIT 1
230+ ) as record_exists
231+ """ .stripMargin,
232+ )
233+ prepared.setString(1 , fireInstanceId.toString)
234+ val rs = prepared.executeQuery()
235+ if (rs.next() && rs.getBoolean(1 )) {
236+ // Record already exists, don't add it again
237+ false
238+ } else {
239+ // Mark this trigger (as already run) in the same transaction, to prevent race conditions where two requests
240+ // trigger the same job
241+ val triggerStartTime : Instant = java.time.Instant .now()
242+ // Same as "addJob()" but without the finish
243+ val prepared = connection.prepareStatement(
244+ """
245+ INSERT INTO job_history(
246+ fire_instance_id,
247+ job_name,
248+ job_group,
249+ trigger_name,
250+ trigger_group,
251+ success,
252+ start
253+ )
254+ VALUES(?, ?, ?, ?, ?, ?, ?)
255+ """ .stripMargin,
256+ )
257+ prepared.setString(1 , fireInstanceId.toString)
258+ prepared.setString(2 , jobKey.getName)
259+ prepared.setString(3 , jobKey.getGroup)
260+ prepared.setString(4 , triggerKey.getName)
261+ prepared.setString(5 , triggerKey.getGroup)
262+ prepared.setBoolean(6 , true )
263+ prepared.setObject(7 , triggerStartTime)
264+ prepared.executeUpdate()
265+ connection.commit()
266+ true
267+ }
268+ } finally {
269+ connection.close()
270+ }
271+ }
272+
273+ def completeOneTimeJob (
274+ fireInstanceId : String ,
275+ fireTime : Instant ,
276+ instanceDurationInMillis : Long ,
277+ success : Boolean ,
278+ ): Unit = {
279+ val connection = getConnection()
280+ try {
281+ val prepared = connection.prepareStatement(
282+ """
283+ UPDATE job_history
284+ SET
285+ success=?,
286+ start=?,
287+ finish=?
288+ WHERE fire_instance_id=?
289+ """ .stripMargin,
290+ )
291+ prepared.setBoolean(1 , success)
292+ prepared.setObject(2 , fireTime)
293+ prepared.setObject(3 , fireTime.plusMillis(instanceDurationInMillis))
294+ prepared.setString(4 , fireInstanceId)
295+ prepared.executeUpdate()
296+ } catch {
297+ case e : Exception => logger.error(" error in recording completion of one-time-job" , e)
298+ } finally {
299+ connection.close()
300+ }
301+ }
193302}
0 commit comments