/* * Copyright (c) 2010-2012 Lockheed Martin Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.eurekastreams.web.client.timer; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.eurekastreams.commons.client.ActionProcessor; import org.eurekastreams.web.client.model.Fetchable; import org.eurekastreams.web.client.ui.Session; import org.eurekastreams.web.client.ui.TimerFactory; import org.eurekastreams.web.client.ui.TimerHandler; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event.NativePreviewEvent; import com.google.gwt.user.client.Event.NativePreviewHandler; /** * Controls timer jobs going back to the server. Bundles HTTP requests for optimizations. * */ public class Timer { /** There are 60,000 milliseconds in a minute due to the theory of time. */ private static final int MS_IN_MIN = 60000; /** Milliseconds in a second (according to the definitions in the metric system). */ private static final long MS_IN_SEC = 1000; /** Seconds in a minute. */ private static final long SEC_IN_MIN = 60; /** Poll period of the master driving timer. */ private static final int MASTER_TIMER_POLL_MS = (int) (30 * MS_IN_SEC); /** Max time without user activity to consider system idle. */ private static final long IDLE_TIMEOUT_MS = 5 * MS_IN_MIN; /** How close is close enough for letting a job batch run (so it doesn't have to wait for the next master timer). */ private static final long TIMER_MERCY_SEC = 5; /** * The fetchable models. */ private final HashMap<String, Fetchable> fetchables = new HashMap<String, Fetchable>(); /** * The jobs. */ private final HashMap<Integer, Set<String>> jobs = new HashMap<Integer, Set<String>>(); /** * The requests. */ private final HashMap<String, Serializable> requests = new HashMap<String, Serializable>(); /** * The list of paused jobs. */ private final Set<String> pausedJobs = new HashSet<String>(); /** * Temp jobs. Delete these when the page changes. */ private final Set<String> tempJobs = new HashSet<String>(); /** Last time each set of jobs (i.e. 1 minute jobs, 2 minute jobs, etc.) was run. */ private final HashMap<Integer, Date> lastRunTimes = new HashMap<Integer, Date>(); /** Master driving timer. */ private com.google.gwt.user.client.Timer masterTimer; /** If there has been activity since the last master timer poll. */ private boolean activity = false; /** If the system is officially idle. */ private boolean idle = false; /** Logger. */ private Logger log; /** * Constructor. */ public Timer() { // This setup is to handle unit testing. GWT Logging uses GWT.Create to set up the logging implementation, // however GWT.Create is not available when running unit tests. (It returns null if GWTMockUtilities.disarm is // called or throws an exception if not. Thus during unit tests calling LogConfiguration.loggingIsEnabled throws // a NullPointerException. try { if (LogConfiguration.loggingIsEnabled()) { log = Logger.getLogger("org.eurekastreams.web.client.timer.Timer"); } } catch (NullPointerException ex) { // redundant, but keeps checkstyle happy log = null; } } /** * Set up the timer and activity monitoring. */ private void initialize() { masterTimer = new TimerFactory().createTimer(new TimerHandler() { private Date lastActivity = new Date(); public void run() { try { // idle checking if (activity) { // there has been user activity since the last poll, so record the time lastActivity = new Date(); activity = false; } else { // no activity since last poll, see if it's long enough to be considered idle long lastActivityMs = lastActivity.getTime(); long nowMs = new Date().getTime(); if (nowMs - lastActivityMs > IDLE_TIMEOUT_MS) { // idle: shut down master timer and don't run the jobs idle = true; masterTimer.cancel(); return; } } // run timers runTimerJobs(); } catch (Exception ex) { if (log != null) { log.log(Level.SEVERE, "Exception thrown in master timer handler.", ex); } } } }); Event.addNativePreviewHandler(new NativePreviewHandler() { /* * This event will be called A LOT. Do not add anything non-trivial to it! It is used to determine * inactivity. */ public void onPreviewNativeEvent(final NativePreviewEvent event) { if (event.getTypeInt() == Event.ONMOUSEMOVE || event.getTypeInt() == Event.ONKEYDOWN) { // mark that there has been activity. let master timer record the time on its next poll. activity = true; // if system is idle, mark as not idle and get jobs restarted if (idle) { idle = false; // defer this to prevent making the user event slow Scheduler.get().scheduleDeferred(new ScheduledCommand() { public void execute() { try { runTimerJobs(); masterTimer.scheduleRepeating(MASTER_TIMER_POLL_MS); } catch (Exception ex) { if (log != null) { log.log(Level.SEVERE, "Exception thrown when resuming from idle.", ex); } } } }); } } } }); // masterTimer.scheduleRepeating(MASTER_TIMER_POLL_MS); } /** * Checks for all timer jobs (of any periodicity) that are eligible to run and runs them. */ private void runTimerJobs() { Date now = new Date(); long nowMs = now.getTime(); boolean anyJobsRun = false; ActionProcessor actionProcessor = Session.getInstance().getActionProcessor(); for (int numMinutes : jobs.keySet()) { // has it been more that numMinutes since this batch last ran? Date lastRun = lastRunTimes.get(numMinutes); boolean okToRun = true; if (lastRun != null) { long deltaSec = (nowMs - lastRun.getTime()) / MS_IN_SEC; long requiredSec = numMinutes * SEC_IN_MIN - TIMER_MERCY_SEC; okToRun = deltaSec >= requiredSec; } if (okToRun) { for (String job : jobs.get(numMinutes)) { if (!anyJobsRun) { anyJobsRun = true; actionProcessor.setQueueRequests(true); } try { if (fetchables.containsKey(job) && !pausedJobs.contains(job)) { fetchables.get(job).fetch(requests.get(job), false); } } catch (Exception ex) { // Just making sure ANYTHING that goes wrong doesn't hose the entire app. int x = 0; } } // mark this batch as having run (using the base lastRunTimes.put(numMinutes, now); } } if (anyJobsRun) { actionProcessor.setQueueRequests(false); actionProcessor.fireQueuedRequests(); } } /** * Add a timer job. * * @param jobKey * the job key, used for lookup and modification. DON'T FORGET IT. * @param numOfMinutes * the number of minutes to wait between executions. * @param fetchable * the fetchable client model. * @param request * the request. * @param permanant * does this timer job persist. */ public void addTimerJob(final String jobKey, final Integer numOfMinutes, final Fetchable fetchable, final Serializable request, final boolean permanant) { if (!permanant) { tempJobs.add(jobKey); } if (!jobs.containsKey(numOfMinutes)) { jobs.put(numOfMinutes, new HashSet<String>()); } jobs.get(numOfMinutes).add(jobKey); requests.put(jobKey, request); fetchables.put(jobKey, fetchable); if (masterTimer == null) { initialize(); } } /** * Change a request object for a job. * * @param jobKey * the job key. * @param request * the request. */ public void changeRequest(final String jobKey, final Serializable request) { requests.put(jobKey, request); } /** * Change the fetchable for a job. * * @param jobKey * the job key. * @param fetchable * the fetchable. */ public void changeFetchable(final String jobKey, final Fetchable fetchable) { fetchables.put(jobKey, fetchable); } /** * Pause a job. * * @param jobKey * the job to pause. */ public void pauseJob(final String jobKey) { pausedJobs.add(jobKey); } /** * Unpause a job. * * @param jobKey * the job to unpause. */ public void unPauseJob(final String jobKey) { pausedJobs.remove(jobKey); } /** * Clean up the temporary jobs. Called each time the page changes. */ public void clearTempJobs() { for (String job : tempJobs) { removeTimerJob(job); } } /** * Remove a job. * * @param jobKey * the job key. */ public void removeTimerJob(final String jobKey) { for (Integer min : jobs.keySet()) { for (String job : jobs.get(min)) { if (job.equals(jobKey)) { jobs.get(min).remove(jobKey); break; } } } } }