Skip to content

Commit

Permalink
Adds create, update, delete of a schedule and a job scheduling at once (
Browse files Browse the repository at this point in the history
#81)

* Adds new unscheduleJob method.

Manages both job and schedule references removal.

* Add public create, update, delete methods on JobSchedule.

These methods are intended to provide simple JobSchedule operations
with consistent naming. Job and schedule concepts are managed all at
once.

* Removed unnecessary comments.

Warning: Don't know if version name change is wise. Use to
publish-local artifact to test integration in other projects.

* Fix wrong @return documentation.

* WIP - updateJobSchedule and deleteJobSchedule tests fails.

* Fix test cron expressions to match expected result. All tests pass!

* Typo

* Add test for non existing job schedule deletion.

* Remove test artifact version modification.

* Added usage Dynamic create, update, delete `JobSchedule` operations
sub-section.
  • Loading branch information
refond authored and enragedginger committed Feb 8, 2019
1 parent c1e62e9 commit 7d915d0
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ project/plugins/project/
.idea_modules
*.iws
*.ipr
/bin/
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,93 @@ OnlyBusinessHours {
}
```


### Dynamic create, update, delete `JobSchedule` operations

These `JobSchedule` operations let you programatically manage job and job scheduling
all at once.
*For working examples please check test section:*
*"The Quartz Scheduling Extension with Dynamic create, update, delete JobSchedule operations"*
*in* `com.typesafe.akka.extension.quartz.QuartzSchedulerFunctionalSpec`


#### Create JobSchedule

`createJobSchedule` let you create a new job and schedule it all at once:


```scala
val scheduleJobName : String = "myJobName_1"
val messageReceiverActor: ActorRef = myActorRef
val messageSentToReceiver : AnyRef = myCaseClassMessage
val scheduleCronExpression: String = "*/10 * * ? * *" // Will fire every ten seconds

try {
scheduler.createJobSchedule(
name = scheduleJobName,
receiver = messageReceiverActor,
msg = messageSentToReceiver,
cronExpression = scheduleCronExpression)
} catch {
case iae: IllegalArgumentException => iae // Do something useful with it.
}
```

In addition you can specify the following optional description, calendar and timezone parameters:

```scala
val scheduleJobDescriptionOpt : Option[String] = Some("Scheduled job for test purposes.")
val aCalendarName: Option[String] = Some("HourOfTheWolf")
val aTimeZone: java.util.TimeZone = java.util.TimeZone.getTimeZone("UTC")

try {
scheduler.createJobSchedule(
name = scheduleJobName,
receiver = messageReceiverActor,
msg = messageSentToReceiver,
cronExpression = scheduleCronExpression,
description = Some(job.description),
calendar = aCalendarName,
timezone = aTimeZone
)
} catch {
case iae: IllegalArgumentException => iae // Do something useful with it.
}
```

#### Update JobSchedule

`updateJobSchedule` has exactely the same signature as create JobSchedule but tries to perform
an update of an existing `scheduleJobName`

```scala
try {
scheduler.updateJobSchedule(
name = scheduleJobName,
receiver = messageReceiverActor,
msg = messageSentToReceiver,
cronExpression = scheduleCronExpression,
description = Some(job.description),
calendar = aCalendarName,
timezone = aTimeZone
)
} catch {
case iae: IllegalArgumentException => iae // Do something useful with it.
}
```

#### Delete JobSchedule

```scala
try {
if (scheduler.deleteJobSchedule(name = scheduleJobName)) {
// Do something if deletion succeeded
} else {
// Do something else if deletion failed
}
} catch {
case e: Exception =>
// Take action in case an exception is thrown
}
```

68 changes: 68 additions & 0 deletions src/main/scala/QuartzSchedulerExtension.scala
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,74 @@ class QuartzSchedulerExtension(system: ExtendedActorSystem) extends Extension {
// TODO - Exception checking?
}

/**
* Creates job, associated triggers and corresponding schedule at once.
*
*
* @param name The name of the job, as defined in the schedule
* @param receiver An ActorRef, who will be notified each time the schedule fires
* @param msg A message object, which will be sent to `receiver` each time the schedule fires
* @param description A string describing the purpose of the job
* @param cronExpression A string with the cron-type expression
* @param calendar An optional calendar to use.
* @param timezone The time zone to use if different from default.
* @return A date which indicates the first time the trigger will fire.
*/
def createJobSchedule(
name: String, receiver: ActorRef, msg: AnyRef, description: Option[String] = None,
cronExpression: String, calendar: Option[String] = None, timezone: TimeZone = defaultTimezone) = {
createSchedule(name, description, cronExpression, calendar, timezone)
schedule(name, receiver, msg)
}

/**
* Updates job, associated triggers and corresponding schedule at once.
*
*
* @param name The name of the job, as defined in the schedule
* @param receiver An ActorRef, who will be notified each time the schedule fires
* @param msg A message object, which will be sent to `receiver` each time the schedule fires
* @param description A string describing the purpose of the job
* @param cronExpression A string with the cron-type expression
* @param calendar An optional calendar to use.
* @param timezone The time zone to use if different from default.
* @return A date which indicates the first time the trigger will fire.
*/
def updateJobSchedule(
name: String, receiver: ActorRef, msg: AnyRef, description: Option[String] = None,
cronExpression: String, calendar: Option[String] = None, timezone: TimeZone = defaultTimezone): Date = {
rescheduleJob(name, receiver, msg, description, cronExpression, calendar, timezone)
}

/**
* Deletes job, associated triggers and corresponding schedule at once.
*
* Remark: Identical to `unscheduleJob`. Exists to provide consistent naming of related JobSchedule operations.
*
* @param name The name of the job, as defined in the schedule
*
* @return Success or Failure in a Boolean
*/
def deleteJobSchedule(name: String): Boolean = unscheduleJob(name)

/**
* Unschedule an existing schedule
*
* Cancels the running job and all associated triggers and removes corresponding
* schedule entry from internal schedules map.
*
* @param name The name of the job, as defined in the schedule
*
* @return Success or Failure in a Boolean
*/
def unscheduleJob(name: String): Boolean = {
val isJobCancelled = cancelJob(name)
if (isJobCancelled) { removeSchedule(name) }
isJobCancelled
}

/**
* Create a schedule programmatically (must still be scheduled by calling 'schedule')
*
Expand Down
118 changes: 118 additions & 0 deletions src/test/scala/QuartzSchedulerFunctionalSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,124 @@ class QuartzSchedulerFunctionalSpec(_system: ActorSystem) extends TestKit(_syste
receipt must have size(5)
}
}


/**
* JobSchedule operations {create, update, delete} combine existing
* QuartzSchedulerExtension {createSchedule, schedule, rescheduleJob}
* and adds deleleteJobSchedule (unscheduleJob synonym created for naming
* consistency with existing rescheduleJob method).
*/
"The Quartz Scheduling Extension with Dynamic create, update, delete JobSchedule operations" must {
"Throw exception if creating job schedule that already exists" in {

val alreadyExistingScheduleJobName = "cronEvery10Seconds"
val receiver = _system.actorOf(Props(new ScheduleTestReceiver))
val probe = TestProbe()
receiver ! NewProbe(probe.ref)

an [IllegalArgumentException] must be thrownBy {
QuartzSchedulerExtension(_system).createJobSchedule(alreadyExistingScheduleJobName, receiver, Tick, None, "*/10 * * ? * *", None)
}
}

"Throw exception if creating a scheduled job with schedule that has invalid cron expression" in {

// Remark: Tests are not completely in isolation as using "nonExistingCron"
// schedule name here would fail because of use and definition in former:
// "Add new, schedulable schedule with valid inputs" test.
val nonExistingScheduleJobName = "nonExistingCron_2"

val receiver = _system.actorOf(Props(new ScheduleTestReceiver))

an [IllegalArgumentException] must be thrownBy {
QuartzSchedulerExtension(_system).createJobSchedule("nonExistingCron_2", receiver, Tick, None, "*/10 x * ? * *", None)
}
}

"Add new, schedulable job and schedule with valid inputs" in {
val receiver = _system.actorOf(Props(new ScheduleTestReceiver))
val probe = TestProbe()
receiver ! NewProbe(probe.ref)

val jobDt = QuartzSchedulerExtension(_system).createJobSchedule("nonExistingCron_2", receiver, Tick, Some("Creating new dynamic schedule"), "*/1 * * ? * *", None)

/* This is a somewhat questionable test as the timing between components may not match the tick off. */
val receipt = probe.receiveWhile(Duration(30, SECONDS), Duration(15, SECONDS), 5) {
case Tock =>
Tock
}

receipt must contain(Tock)
receipt must have size(5)
}

"Reschedule an existing job schedule Cron Job" in {

val toRescheduleJobName = "toRescheduleCron_1"

val receiver = _system.actorOf(Props(new ScheduleTestReceiver))
val probe = TestProbe()
receiver ! NewProbe(probe.ref)

val jobDt = QuartzSchedulerExtension(_system).createJobSchedule(toRescheduleJobName, receiver, Tick, Some("Creating new dynamic schedule for updateJobSchedule test"), "*/4 * * ? * *")

noException should be thrownBy {
val newFirstTimeTriggerDate = QuartzSchedulerExtension(_system).updateJobSchedule(toRescheduleJobName, receiver, Tick, Some("Updating new dynamic schedule for updateJobSchedule test"), "42 * * ? * *")
val jobCalender = Calendar.getInstance()
jobCalender.setTime(newFirstTimeTriggerDate)
jobCalender.get(Calendar.SECOND) mustEqual 42
}
}


"Delete an existing job schedule Cron Job without any error and allow successful creation of new schedule with identical job name" in {

val toDeleteSheduleJobName = "toBeDeletedscheduleCron_1"

val receiver = _system.actorOf(Props(new ScheduleTestReceiver))
val probe = TestProbe()
receiver ! NewProbe(probe.ref)

val jobDt = QuartzSchedulerExtension(_system).createJobSchedule(toDeleteSheduleJobName, receiver, Tick, Some("Creating new dynamic schedule for deleteJobSchedule test"), "*/7 * * ? * *")

noException should be thrownBy {
// Delete existing scheduled job
val success = QuartzSchedulerExtension(_system).deleteJobSchedule(toDeleteSheduleJobName)
if (success) {

// Create a new schedule job reusing former toDeleteSheduleJobName. This will fail if delebeJobSchedule is not effective.
val newJobDt = QuartzSchedulerExtension(_system).createJobSchedule(toDeleteSheduleJobName, receiver, Tick, Some("Creating new dynamic schedule after deleteJobSchedule success"), "8 * * ? * *")
val jobCalender = Calendar.getInstance()
jobCalender.setTime(newJobDt)
jobCalender.get(Calendar.SECOND) mustEqual 8
} else {
fail(s"deleteJobSchedule(${toDeleteSheduleJobName}) expected to return true returned false.")
}
}
}

"Delete a non existing job schedule Cron Job with no error and a return value false" in {

val nonExistingCronToBeDeleted = "nonExistingCronToBeDeleted"

val receiver = _system.actorOf(Props(new ScheduleTestReceiver))
val probe = TestProbe()
receiver ! NewProbe(probe.ref)

noException should be thrownBy {
// Deleting non existing scheduled job
val success = QuartzSchedulerExtension(_system).deleteJobSchedule(nonExistingCronToBeDeleted)
// must return false
if (success) {
fail(s"deleteJobSchedule(${nonExistingCronToBeDeleted}) expected to return false returned true.")
}
}
}



}


case class NewProbe(probe: ActorRef)
Expand Down

0 comments on commit 7d915d0

Please sign in to comment.