The scheduler branch contains a scheduler component (
org.osaf.cosmo.scheduler.*) that allows code to be run one some interval on the server, without any user interaction. The main use case for this is to provide notification reports to users that contain upcoming information gathered from the user's collections.
SchedulerImpl
The meat of the scheduler is in the
SchedulerImpl class. The
SchedulerImpl relies on 3 main components: a
UserScheduleManager, a map of
JobTypeScheduler implementations, and the Quartz scheduler engine. All components are configured in spring, making adding to or extending the framework easy.
Quartz Scheduler Engine
The quartz engine provides the basic scheduling building blocks for a scheduler. It allows a job (code) to be scheduled and executed at configurable time intervals, modeled by
Trigger s, and manages a pool of threads that are responsible for executing jobs.
UserScheduleManager
The
UserScheduleManager interface provides an api for managing user's schedules. A user scheduler is modeled by the
Schedule class. A
Schedule contains a name and set of properties. The properties for a schedule are key/value pairs and vary depending on the type of schedule. This allows for different types of schedules with different properties to be supported. The
UserScheduleManager is responsible for managing
Schedule objects including determining which users have schedules, what those schedules are, as well as providing an api for operations such as disabling a schedule. The prototype implementation for
UserScheduleManager is
UserPreferencesScheduleManager which assumes schedules are stored as user preferences. One of the advantages of using user preferences is that no new persistence work or protocol work has to be done, as its already supported for user preferences.
UserPreferencesScheduleManager
UserPreferencesScheduleManager is the default implementation of
UserScheduleManager that uses user preferences to store schedules. It assumes the following:
- user schedules are enabled if the preference cosmo.scheduler.enabled=true is present
- a schedule is define by the set of properties that start with cosmo.scheduler.job.[jobName].
- a schedule is enabled if the preference cosmo.scheduler.job.[jobName].enabled=true is present
- a schedule must include a "type" property like cosmo.scheduler.job.[jobName].type=[typeKey] where [typeKey] maps to a
JobTypeScheduler that is responsible for scheduling that job type (explained later)
What this means is that all schedules are stored as user preferences (key/value pairs associated to a user). For example the following set of user preferences defines a single schedule named "1" that is handled by the
JobTypeScheduler mapped to the key "dummy" and contains extra properties "message" and "notifier.name" (explained later).
| key | value |
| cosmo.scheduler.enable | true |
| cosmo.scheduler.job.1.enabled | true |
| cosmo.scheduler.job.1.type | dummy |
| cosmo.scheduler.job.1.cronexp | 5 * * * * ? |
| cosmo.scheduler.job.1.message | Hello World! |
| cosmo.schedluer.job.1.notifier.name | log |
This translates into the following
Schedule object:
- name = 1
- properties = { cronexp=>"5 * * * * ?" , message=> "Hello World!", notifier.name=>"log" }
Multiple schedules can be defined for a user by using a different jobName. Schedules can be enabled or disabled individually by setting the
cosmo.scheduler.[jobName].enabled property. All schedules can be disabled by setting the
cosmo.scheduler.enabled to
false.
The server already provides atom protocol support for updating user preferences, so schedules can be added/removed/updated using this protocol.
JobTypeScheduler
The next piece in the puzzle is the
JobTypeScheduler which is responsible for translating a
Schedule object into runnable code that runs at a specific interval. Basically, it translates a set of properties into a quartz job. For the example above, the
JobTypeScheduler DummyJobTypeScheduler , is registered with the
SchedulerImpl under the key "dummy". This schedule object represented by the above properties will be passed, along with the user and quartz engine to the
DummyJobTypeScheduler . The
DummyJobTypeScheduler implementation simply grabs the "cronexp" property and creates a quartz
org.quartz.CronTrigger and schedules a job in the quartz engine that ends up logging the value of the "message" property on an interval defined by the "cronexp", which in the example above means every minute on the 5th second.
It is important to note that a
JobTypeScheduler is responsible for interfacing with the quartz scheduler engine in order to schedule the job. This means it needs to create a
org.quartz.JobDetail, which represents a quartz job, and schedule it using a
org.quartz.Trigger . This requires knowledge of how quartz works, but provides the most flexibility.
Implementing new types of Schedules
So to recap, the scheduler component allows schedules associated to a user to be setup and run. These schedules are defined by a name and a set of key/value properties. An associated
JobTypeScheduler is responsible for translating the properties into a job that runs in the quartz scheduler. So the steps in supporting a new type of schedule are:
- define properties for schedule type (reserved properties are enabled=? and type=?)
- implement
JobTypeScheduler for the schedule type and register with SchedulerImpl
- have the client manage schedules using user preferences api's/protocols
ForwardLookingJobTypeScheduler
The initial schedule type that will be supported in cosmo is the forward-looking notification job. This is a job that runs on a daily or weekly interval and queries a set of collections and returns the results in the form of an email notification. The properties for this type of schedule are:
| name | value | description |
| type | forward | constant, must be present |
| enabled | true or false | constant, must be present |
| reportType | daily or weekly | defines time period to query, either 1 day forward or 1 week forward |
| timezone | America/Chicago | user's timezone, used to determine when to send report and how to interpret floating events. Optional, if not present, the server timezone is assumed |
| cronexp | 0 0 6 ? * SUN (weekly) or 0 0 6 ? * MON-FRI (daily) | defines the interval (examples are shown for daily and weekly interval that occurs at 6am |
| collection.UID1 | true | include collection with uid UID1 in results |
| collection.UID2 | true | include collection with uid UID2 in results |
| notifier.name | email | send results to Notifier "email" |
| notifier.email.addresses | user1@foo.org,user2@foo.org | email addresses to send results to. Optional. If not present, the report will be sent to the user's email address |
Assuming the use of the
UserPreferencesScheduleManager this schedule would be represented as the following user preferences:
| key | value |
| cosmo.scheduler.enable | true |
| cosmo.scheduler.job.1.enabled | true |
| cosmo.scheduler.job.1.type | forward |
| cosmo.scheduler.job.1.reportType | weekly |
| cosmo.scheduler.job.1.timezone | America/Chicago |
| cosmo.scheduler.job.1.cronexp | 0 0 6 ? * SUN |
| cosmo.scheduler.job.1.collection.UID1 | true |
| cosmo.scheduler.job.1.collection.UID2 | true |
| cosmo.schedluer.job.1.notifier.name | email |
| cosmo.scheduler.job.1.notifier.email.addresses | user1@foo.org,user2@foo.org |
So in order to add/remove/update the schedule for a user, the service and protocol apis for user preferences can be used as long as the preferences conform to the above format.
ForwardLookingNotificationJob
The
ForwardLookingJobTypeScheduler schedules jobs of type
ForwardLookingNotificationJob into the quartz engine. This job extends from a hierarchy starting with
Job, which is an abstract spring
QuartzJobBean. By extending
QuartzJobBean, properties stored in the quartz
JobDetail can be injected into the job bean automatically. For example, instead of having to do something like:
String username = context.getJobDetail().getJobDataMap().getString("username");
The job class can define a username setter like:
public class MyJob extends Job {
private String username;
public void setUsername(String username) {
this.username = username;
}
}
and the spring
QuartzJobBean will automatcally populate username from the jobDataMap and scheduler context map.
The base
Job class can be configured to execute a set of
Filter objects, which allow code to be executed before/after a job. Just like servlet filters, job filters are executed in a filter chain. Most user jobs on the server are going to want at least 2 filters to execute before each job. The first filter is the
HibernateSessionFilter, which sets up and tears down a hibernate session, which mimics the OpenSessionInView pattern used in the rest of cosmo. This allows for things like lazy loading in hibernate to work. The second filer is the
SecurityContextFilter, which initializes the security context with the owner of the job. This allows the job to be executed as the user and allows security checks to be performed on that user while the job is executed. This prevents a user job from accessing a items the user does not have access to while at the same time leveraging existing auth code.
ServiceJob injects a
ContentService into the job, allowing that api to be used during job execution. The
ContentService is injected by being added to the quartz schedule context map, which makes it available to each job.
NotificationJob generates a report, represented by the
Report interface and sends the results to a
Notifier.
NotificationJob has a set of
Notifier implementations indexed by name. In the above schedule properties, the property
notifier.name=email tells the
NotificationJob to send the results along with a set of "notifier" properties to the
Notifier implementation mapped by the key "email". In this example the notifier properties is the map {email.addresses=> "user1@foo.org,user2@foo.org"}. The set of
Notifiers is present in the quartz schedule context map, this it is injected to each
NotificationJob.
MutlipleCollectionJob adds a list of collection uids representing the collections that the job is working on.
Then there is
ForwardLookingNotificationJob, the only non-abstract job type listed here, which simply implements
generateReport(), returning a
ForwardLookingReport based on the schedule parameters, to be passed to the specified
Notifier instance.
Cosmo will initially provide a notifier that sends email, but it would be easy to plugin additional notifiers that for example use XMPP or SMS in the future.
The flow for a
ForwardLookingNotificationJob execution is:
-
Job creates filter chain from list of Filters
-
HibernateSessionFilter sets up hibernate session
-
SecurityContextFilter sets up security context
-
ForwardLookingNotificationJob.generateReport() generates ForwardLookingReport
- report is passed to notifier, user is notified of results (email)
-
SecurityContextFilter tears down security Context
-
HibernateSessionFilter tears down hibernate session
EmailNotifier
Cosmo provides a
Notifier implementation which takes results from a
ForwardLookingNotificationJob (in the form of a
ForwardLookingReport ) and sends email to either the user or a set of email addresses defined in the notifier property
email.addresses. The implementation uses a spring
JavaMailSender and the Velocity template engine to generate and send emails.
SchedulerImpl Notes
Configuration
All configuration is done in applicationContext.xml
Startup
The scheduler is started when the spring context is loaded.
Changes in User Schedules
User schedules are refreshed on a configurable interval (default 1 hour). Part of initialization includes scheduling a job with the quartz engine with a simple trigger that calls refresh() each time its fired. Ideally the server provides a way for components to "listen" for certain events such as "user updated" in order to provide a more efficient means of handling schedule updates.
--
RandyLetness - 07 May 2008