// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved // http://code.google.com/p/narya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.presents.server; import java.util.Calendar; import com.samskivert.util.Calendars; import com.samskivert.util.HashIntMap; import com.samskivert.util.Interval; import com.samskivert.util.ObserverList; import com.samskivert.util.StringUtil; import com.samskivert.util.Calendars.Builder; import com.threerings.util.MessageBundle; import com.threerings.presents.dobj.RootDObjectManager; import static com.threerings.presents.Log.log; /** * Handles scheduling and execution of automated server reboots. Note that this service simply * shuts down the server and assumes it will be automatically restarted by some external entity. It * is generally useful to run a server from a script that automatically restarts it when it * terminates. */ public abstract class RebootManager { /** * An interface for receiving notifications about pending automated shutdowns. */ public static interface PendingShutdownObserver { /** * Called prior to an automated shutdown. NOTE that the shutdown is not guaranteed to * happen, this interface is merely for notification purposes. * * @param warningsLeft The number of warnings left prior to the shutdown. This value will * be 0 on the last warning, usually 1-2 minutes prior to the actual shutdown. * @param msLeft the approximate number of milliseconds left prior to the shutdown. */ void shutdownPlanned (int warningsLeft, long msLeft); } /** The default warning times. */ public static final int[] DEFAULT_WARNINGS = { 30, 20, 15, 10, 5, 2 }; /** * Finishes initialization of the manager. */ public void init () { scheduleRegularReboot(); } /** * Schedules our next regularly scheduled reboot. * * @return true if a reboot was scheduled, false if regularly scheduled reboots are disabled. */ public boolean scheduleRegularReboot () { // maybe schedule an automatic reboot based on our configuration int freq = getDayFrequency(); if (freq == -1) { return false; } Builder cal = Calendars.now().zeroTime().addHours(getRebootHour()).addDays(freq); // maybe avoid weekends if (getSkipWeekends()) { int dow = cal.get(Calendar.DAY_OF_WEEK); switch (dow) { case Calendar.SATURDAY: if (freq > 1) { cal.addDays(-1); } break; case Calendar.SUNDAY: if (freq > 2) { cal.addDays(-2); } break; } } scheduleReboot(cal.toTime(), AUTOMATIC_INITIATOR); return true; } /** * Is the manager planning a shutdown in the near future? */ public boolean willShutdownSoon () { return _rebootSoon; } /** * Add an observer to the observer list. */ public void addObserver (PendingShutdownObserver observer) { _observers.add(observer); } /** * Schedules a reboot for the specified time. */ public void scheduleReboot (long rebootTime, String initiator) { // if there's already a reboot scheduled, cancel it if (_interval != null) { _interval.cancel(); _interval = null; } // note our new reboot time and its initiator _nextReboot = rebootTime; _initiator = initiator; // see if the reboot is happening within the time specified by the // longest warning; if so, issue the appropriate warning long now = System.currentTimeMillis(); int[] warnings = getWarnings(); for (int ii = warnings.length - 1; ii >= 0; ii--) { long warnTime = warnings[ii] * 60 * 1000; if (now + warnTime >= _nextReboot) { doWarning(ii); return; } } // otherwise, it's further off; schedule an interval to wake up when we // should issue the first pre-reboot warning _rebootSoon = false; long firstWarnTime = (_nextReboot - (warnings[0] * 60 * 1000)) - now; (_interval = _omgr.newInterval(new Runnable() { public void run () { doWarning(0); } })).schedule(firstWarnTime); } /** * Called by an entity that would like to prevent a reboot. */ public int preventReboot (String whereFrom) { if (whereFrom == null) { throw new IllegalArgumentException("whereFrom must be descriptive."); } int lockId = _nextRebootLockId++; _rebootLocks.put(lockId, whereFrom); return lockId; } /** * Release a reboot lock. */ public void allowReboot (int lockId) { if (null == _rebootLocks.remove(lockId)) { throw new IllegalArgumentException("no such lockId (" + lockId + ")"); } } /** * Returns the minutes at which we give warnings. The last value is also the minimum time at * which we can possibly reboot after the value of the nextReboot field is changed, to prevent * accidentally causing instant server reboots. */ public int[] getWarnings () { return DEFAULT_WARNINGS; } /** * Provides us with our dependencies. */ protected RebootManager (PresentsServer server, RootDObjectManager omgr) { _server = server; _omgr = omgr; } /** * Broadcasts a message to everyone on the server. The following messages will be broadcast: * */ protected abstract void broadcast (String message); /** * Returns the frequency in days of our automatic reboots, or -1 to disable automatically * scheduled reboots. */ protected abstract int getDayFrequency (); /** * Returns the desired hour at which to perform our reboot. */ protected abstract int getRebootHour (); /** * Returns true if the reboot manager should avoid scheduling automated reboots on the * weekends. */ protected abstract boolean getSkipWeekends (); /** * Returns a custom message to be used when broadcasting a pending reboot. */ protected abstract String getCustomRebootMessage (); /** * Composes the given reboot message with the minutes and either the custom or standard details * about the pending reboot. */ protected String getRebootMessage (String key, int minutes) { String msg = getCustomRebootMessage(); if (StringUtil.isBlank(msg)) { msg = "m.reboot_msg_standard"; } return MessageBundle.compose(key, MessageBundle.taint("" + minutes), msg); } /** * Do a warning, schedule the next. */ protected void doWarning (final int level) { _rebootSoon = true; int[] warnings = getWarnings(); if (level == warnings.length) { if (checkLocks()) { return; } // that's it! do the reboot log.info("Performing automatic server reboot/shutdown, as scheduled by: " + _initiator); broadcast("m.rebooting_now"); // wait 1 second, then do it new Interval(Interval.RUN_DIRECT) { @Override public void expired () { _server.queueShutdown(); // this posts a LongRunnable } }.schedule(1000); return; } // issue the warning int minutes = warnings[level]; broadcast(getRebootMessage("m.reboot_warning", minutes)); if (level < warnings.length - 1) { minutes -= warnings[level + 1]; } // schedule the next warning (_interval = _omgr.newInterval(new Runnable() { public void run () { doWarning(level + 1); } })).schedule(minutes * 60 * 1000); notifyObservers(level); } /** * Check to see if there are outstanding reboot locks that may delay the reboot, returning * false if there are none. */ protected boolean checkLocks () { if (_rebootLocks.isEmpty()) { return false; } log.info("Reboot delayed due to outstanding locks", "locks", _rebootLocks.elements()); broadcast("m.reboot_delayed"); (_interval = _omgr.newInterval(new Runnable() { public void run () { doWarning(getWarnings().length); } })).schedule(60 * 1000); return true; } /** * Notify all PendingShutdownObservers of the pending shutdown! */ protected void notifyObservers (int level) { int[] warnings = getWarnings(); final int warningsLeft = warnings.length - level - 1; final long msLeft = 1000L * 60L * warnings[level]; _observers.apply(new ObserverList.ObserverOp() { public boolean apply (PendingShutdownObserver observer) { observer.shutdownPlanned(warningsLeft, msLeft); return true; } }); } /** The server that we're going to reboot. */ protected PresentsServer _server; /** Our distributed object manager. */ protected RootDObjectManager _omgr; /** The time at which our next reboot is scheduled or 0L. */ protected long _nextReboot; /** The entity that scheduled the reboot. */ protected String _initiator; /** True if the reboot is coming soon, within the earliest warning. */ protected boolean _rebootSoon = false; /** The interval scheduled to perform the next step the reboot process. */ protected Interval _interval; /** A list of PendingShutdownObservers. */ protected ObserverList _observers = ObserverList.newFastUnsafe(); /** The next reboot lock id. */ protected int _nextRebootLockId = 0; /** Things that can delay the reboot. */ protected HashIntMap _rebootLocks = new HashIntMap(); protected static final String AUTOMATIC_INITIATOR = "automatic"; }