001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import org.opencms.db.CmsSecurityManager;
031import org.opencms.publish.CmsPublishJobRunning;
032import org.opencms.publish.CmsPublishManager;
033import org.opencms.report.A_CmsReportThread;
034import org.opencms.util.CmsUUID;
035
036import java.util.HashSet;
037import java.util.Hashtable;
038import java.util.Iterator;
039import java.util.Map;
040import java.util.Set;
041
042import org.apache.commons.logging.Log;
043
044/**
045 * The OpenCms "Grim Reaper" thread store were all system Threads are maintained.<p>
046 *
047 * This thread executes all 60 seconds and checks if report threads are still active.
048 * A report thread usually waits for a user to get the contents written to the report.
049 * However, if the user does not request the reports content (e.g. because the
050 * browser was closed), then the report thread becomes abandoned. This Grim Reaper
051 * will collect all such abandoned report threads and remove them after further
052 * 60 seconds.<p>
053 *
054 * Moreover, the Grim Reaper checks for all invalid user sessions that have times out for
055 * 5 or more minutes, and removes them as well.<p>
056 *
057 * @since 6.0.0
058 */
059public final class CmsThreadStore extends Thread {
060
061    /** The log object for this class. */
062    private static final Log LOG = CmsLog.getLog(CmsThreadStore.class);
063
064    /** The number of milliseconds in a minute. */
065    private static final int ONE_MINUTE_IN_MILLIS = 60000;
066
067    /** The interval, in minutes, for checking threads. */
068    private static final int UPDATE_MINUTES_INTERVAL = 5;
069
070    /** Indicates that this thread store is alive. */
071    private boolean m_alive;
072
073    /** The security manager instance. */
074    private CmsSecurityManager m_securityManager;
075
076    /** A map to store all system Threads in. */
077    private Map<CmsUUID, A_CmsReportThread> m_threads;
078
079    /**
080     * Hides the public constructor.<p>
081     *
082     * @param securityManager needed for scheduling "undercover-jobs"
083     *      that increase stability and fault tolerance
084     */
085    protected CmsThreadStore(CmsSecurityManager securityManager) {
086
087        super(new ThreadGroup("OpenCms Thread Store"), "OpenCms: Grim Reaper");
088        setDaemon(true);
089        // Hashtable is still the most efficient form of a synchronized HashMap
090        m_threads = new Hashtable<CmsUUID, A_CmsReportThread>();
091        m_alive = true;
092        m_securityManager = securityManager;
093        start();
094    }
095
096    /**
097     * Adds a Thread to this Thread store.<p>
098     *
099     * @param thread the Thread to add
100     */
101    public void addThread(A_CmsReportThread thread) {
102
103        m_threads.put(thread.getUUID(), thread);
104        if (LOG.isDebugEnabled()) {
105            dumpThreads();
106        }
107    }
108
109    /**
110     * Retrieves a Thread from this Thread store.<p>
111     *
112     * @param key the key of the Thread to retrieve
113     * @return the Thread form this Thread store that matches the given key
114     */
115    public A_CmsReportThread retrieveThread(CmsUUID key) {
116
117        if (LOG.isDebugEnabled()) {
118            dumpThreads();
119        }
120        return m_threads.get(key);
121    }
122
123    /**
124     * @see java.lang.Runnable#run()
125     */
126    @Override
127    public void run() {
128
129        boolean checkPublishQueue = true;
130        int minutesForCheck = 0;
131        while (m_alive) {
132            // the Grim Reaper is eternal, of course
133            try {
134                // one minute sleep time
135                sleep(ONE_MINUTE_IN_MILLIS);
136            } catch (InterruptedException e) {
137                // let's go on reaping...
138            }
139            handleDoomedThreads();
140
141            // check the session manager for invalid sessions not removed for whatever reason
142            minutesForCheck++;
143            if (minutesForCheck < UPDATE_MINUTES_INTERVAL) {
144                continue;
145            }
146            minutesForCheck = 0;
147            checkPublishQueue = !checkPublishQueue;
148
149            // work every n minutes
150            validateSessions();
151            persistData();
152            if (checkPublishQueue) {
153                // every 2*n minutes
154                checkPublishQueue();
155            }
156        }
157    }
158
159    /**
160     * Checks the publish queue for abandoned publish jobs.<p>
161     */
162    protected void checkPublishQueue() {
163
164        // check the publish manager if the current thread is still active
165        try {
166            CmsPublishManager publishManager = OpenCms.getPublishManager();
167            if (publishManager == null) {
168                // this can happen during shutdown
169                return;
170            }
171            // get the current publish job
172            CmsPublishJobRunning publishJob = publishManager.getCurrentPublishJob();
173            if (publishJob == null) {
174                // try to start next job
175                publishManager.checkCurrentPublishJobThread();
176                return;
177            }
178            // get the thread id of the current publish job
179            CmsUUID uid = publishJob.getThreadUUID();
180            if ((uid == null) || (uid.isNullUUID())) {
181                return;
182            }
183            // find the thread
184            A_CmsReportThread thread = m_threads.get(uid);
185            if (thread == null) {
186                return;
187            }
188            // check if the report still has output and so is active
189            if ((System.currentTimeMillis() - thread.getLastEntryTime()) > (UPDATE_MINUTES_INTERVAL
190                * ONE_MINUTE_IN_MILLIS)) {
191                // remove it
192                m_threads.remove(thread);
193                // abandon thread
194                publishManager.abandonThread();
195            }
196        } catch (Throwable t) {
197            LOG.error(Messages.get().getBundle().key(Messages.LOG_THREADSTORE_CHECK_PUBLISH_THREAD_ERROR_0), t);
198        }
199    }
200
201    /**
202     * Handles doomed threads.<p>
203     */
204    protected void handleDoomedThreads() {
205
206        try {
207            Iterator<CmsUUID> i = m_threads.keySet().iterator();
208            Set<CmsUUID> doomed = new HashSet<CmsUUID>();
209            // first collect all doomed Threads
210            while (i.hasNext()) {
211                CmsUUID key = i.next();
212                A_CmsReportThread thread = m_threads.get(key);
213                if (thread.isDoomed()) {
214                    doomed.add(key);
215                    if (LOG.isDebugEnabled()) {
216                        LOG.debug(
217                            Messages.get().getBundle().key(
218                                Messages.LOG_THREADSTORE_DOOMED_2,
219                                thread.getName(),
220                                thread.getUUID()));
221                    }
222                }
223            }
224            i = doomed.iterator();
225            // no remove all doomed Threads from the Thread store
226            while (i.hasNext()) {
227                m_threads.remove(i.next());
228            }
229            if (LOG.isDebugEnabled()) {
230                dumpThreads();
231            }
232        } catch (Throwable t) {
233            // the Grim Reaper must not be stopped by any error
234            LOG.error(Messages.get().getBundle().key(Messages.LOG_THREADSTORE_CHECK_THREADS_ERROR_0), t);
235        }
236    }
237
238    /**
239     * Takes care of the persistence of data normally held in memory.<p>
240     */
241    protected void persistData() {
242
243        try {
244            // save the resource locks to db
245            m_securityManager.writeLocks();
246            // save the log entries to db
247            m_securityManager.updateLog();
248        } catch (Throwable t) {
249            if (LOG.isErrorEnabled()) {
250                LOG.error(
251                    org.opencms.lock.Messages.get().getBundle().key(org.opencms.lock.Messages.ERR_WRITE_LOCKS_0),
252                    t);
253            }
254        }
255    }
256
257    /**
258     * Shut down this thread store.<p>
259     */
260    protected synchronized void shutDown() {
261
262        m_alive = false;
263        interrupt();
264    }
265
266    /**
267     * Checks for invalid sessions.<p>
268     */
269    protected void validateSessions() {
270
271        try {
272            CmsSessionManager sessionInfoManager = OpenCms.getSessionManager();
273            if (sessionInfoManager != null) {
274                // will be null if only the shell is running
275                sessionInfoManager.validateSessionInfos();
276            }
277        } catch (Throwable t) {
278            LOG.error(Messages.get().getBundle().key(Messages.LOG_THREADSTORE_CHECK_SESSIONS_ERROR_0), t);
279        }
280    }
281
282    /**
283     * Method to dump all currently known Threads.<p>
284     */
285    private void dumpThreads() {
286
287        if (LOG.isDebugEnabled()) {
288            StringBuffer b = new StringBuffer(512);
289            Iterator<CmsUUID> i = m_threads.keySet().iterator();
290            while (i.hasNext()) {
291                CmsUUID key = i.next();
292                A_CmsReportThread thread = m_threads.get(key);
293                b.append(thread.getName());
294                b.append(" - ");
295                b.append(thread.getUUID());
296                b.append('\n');
297            }
298            LOG.debug(
299                Messages.get().getBundle().key(
300                    Messages.LOG_THREADSTORE_POOL_CONTENT_2,
301                    new Integer(m_threads.size()),
302                    b.toString()));
303        }
304    }
305}