1//
2//  ========================================================================
3//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4//  ------------------------------------------------------------------------
5//  All rights reserved. This program and the accompanying materials
6//  are made available under the terms of the Eclipse Public License v1.0
7//  and Apache License v2.0 which accompanies this distribution.
8//
9//      The Eclipse Public License is available at
10//      http://www.eclipse.org/legal/epl-v10.html
11//
12//      The Apache License v2.0 is available at
13//      http://www.opensource.org/licenses/apache2.0.php
14//
15//  You may elect to redistribute this code under either of these licenses.
16//  ========================================================================
17//
18
19package org.eclipse.jetty.server.session;
20
21import java.io.DataInputStream;
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.ObjectInputStream;
27import java.net.URI;
28import java.util.ArrayList;
29import java.util.Iterator;
30import java.util.Map;
31import java.util.Set;
32import java.util.Timer;
33import java.util.TimerTask;
34import java.util.concurrent.ConcurrentHashMap;
35import java.util.concurrent.ConcurrentMap;
36
37import javax.servlet.ServletContext;
38import javax.servlet.http.HttpServletRequest;
39
40import org.eclipse.jetty.server.handler.ContextHandler;
41import org.eclipse.jetty.util.IO;
42import org.eclipse.jetty.util.log.Logger;
43
44
45/* ------------------------------------------------------------ */
46/**
47 * HashSessionManager
48 *
49 * An in-memory implementation of SessionManager.
50 * <p>
51 * This manager supports saving sessions to disk, either periodically or at shutdown.
52 * Sessions can also have their content idle saved to disk to reduce the memory overheads of large idle sessions.
53 * <p>
54 * This manager will create it's own Timer instance to scavenge threads, unless it discovers a shared Timer instance
55 * set as the "org.eclipse.jetty.server.session.timer" attribute of the ContextHandler.
56 *
57 */
58public class HashSessionManager extends AbstractSessionManager
59{
60    final static Logger __log = SessionHandler.LOG;
61
62    protected final ConcurrentMap<String,HashedSession> _sessions=new ConcurrentHashMap<String,HashedSession>();
63    private static int __id;
64    private Timer _timer;
65    private boolean _timerStop=false;
66    private TimerTask _task;
67    long _scavengePeriodMs=30000;
68    long _savePeriodMs=0; //don't do period saves by default
69    long _idleSavePeriodMs = 0; // don't idle save sessions by default.
70    private TimerTask _saveTask;
71    File _storeDir;
72    private boolean _lazyLoad=false;
73    private volatile boolean _sessionsLoaded=false;
74    private boolean _deleteUnrestorableSessions=false;
75
76
77
78
79    /* ------------------------------------------------------------ */
80    public HashSessionManager()
81    {
82        super();
83    }
84
85    /* ------------------------------------------------------------ */
86    /**
87     * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart()
88     */
89    @Override
90    public void doStart() throws Exception
91    {
92        super.doStart();
93
94        _timerStop=false;
95        ServletContext context = ContextHandler.getCurrentContext();
96        if (context!=null)
97            _timer=(Timer)context.getAttribute("org.eclipse.jetty.server.session.timer");
98        if (_timer==null)
99        {
100            _timerStop=true;
101            _timer=new Timer("HashSessionScavenger-"+__id++, true);
102        }
103
104        setScavengePeriod(getScavengePeriod());
105
106        if (_storeDir!=null)
107        {
108            if (!_storeDir.exists())
109                _storeDir.mkdirs();
110
111            if (!_lazyLoad)
112                restoreSessions();
113        }
114
115        setSavePeriod(getSavePeriod());
116    }
117
118    /* ------------------------------------------------------------ */
119    /**
120     * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop()
121     */
122    @Override
123    public void doStop() throws Exception
124    {
125        // stop the scavengers
126        synchronized(this)
127        {
128            if (_saveTask!=null)
129                _saveTask.cancel();
130            _saveTask=null;
131            if (_task!=null)
132                _task.cancel();
133            _task=null;
134            if (_timer!=null && _timerStop)
135                _timer.cancel();
136            _timer=null;
137        }
138
139        // This will callback invalidate sessions - where we decide if we will save
140        super.doStop();
141
142        _sessions.clear();
143
144    }
145
146    /* ------------------------------------------------------------ */
147    /**
148     * @return the period in seconds at which a check is made for sessions to be invalidated.
149     */
150    public int getScavengePeriod()
151    {
152        return (int)(_scavengePeriodMs/1000);
153    }
154
155
156    /* ------------------------------------------------------------ */
157    @Override
158    public int getSessions()
159    {
160        int sessions=super.getSessions();
161        if (__log.isDebugEnabled())
162        {
163            if (_sessions.size()!=sessions)
164                __log.warn("sessions: "+_sessions.size()+"!="+sessions);
165        }
166        return sessions;
167    }
168
169    /* ------------------------------------------------------------ */
170    /**
171     * @return seconds Idle period after which a session is saved
172     */
173    public int getIdleSavePeriod()
174    {
175      if (_idleSavePeriodMs <= 0)
176        return 0;
177
178      return (int)(_idleSavePeriodMs / 1000);
179    }
180
181    /* ------------------------------------------------------------ */
182    /**
183     * Configures the period in seconds after which a session is deemed idle and saved
184     * to save on session memory.
185     *
186     * The session is persisted, the values attribute map is cleared and the session set to idled.
187     *
188     * @param seconds Idle period after which a session is saved
189     */
190    public void setIdleSavePeriod(int seconds)
191    {
192      _idleSavePeriodMs = seconds * 1000L;
193    }
194
195    /* ------------------------------------------------------------ */
196    @Override
197    public void setMaxInactiveInterval(int seconds)
198    {
199        super.setMaxInactiveInterval(seconds);
200        if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000L)
201            setScavengePeriod((_dftMaxIdleSecs+9)/10);
202    }
203
204    /* ------------------------------------------------------------ */
205    /**
206     * @param seconds the period is seconds at which sessions are periodically saved to disk
207     */
208    public void setSavePeriod (int seconds)
209    {
210        long period = (seconds * 1000L);
211        if (period < 0)
212            period=0;
213        _savePeriodMs=period;
214
215        if (_timer!=null)
216        {
217            synchronized (this)
218            {
219                if (_saveTask!=null)
220                    _saveTask.cancel();
221                if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured
222                {
223                    _saveTask = new TimerTask()
224                    {
225                        @Override
226                        public void run()
227                        {
228                            try
229                            {
230                                saveSessions(true);
231                            }
232                            catch (Exception e)
233                            {
234                                __log.warn(e);
235                            }
236                        }
237                    };
238                    _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs);
239                }
240            }
241        }
242    }
243
244    /* ------------------------------------------------------------ */
245    /**
246     * @return the period in seconds at which sessions are periodically saved to disk
247     */
248    public int getSavePeriod ()
249    {
250        if (_savePeriodMs<=0)
251            return 0;
252
253        return (int)(_savePeriodMs/1000);
254    }
255
256    /* ------------------------------------------------------------ */
257    /**
258     * @param seconds the period in seconds at which a check is made for sessions to be invalidated.
259     */
260    public void setScavengePeriod(int seconds)
261    {
262        if (seconds==0)
263            seconds=60;
264
265        long old_period=_scavengePeriodMs;
266        long period=seconds*1000L;
267        if (period>60000)
268            period=60000;
269        if (period<1000)
270            period=1000;
271
272        _scavengePeriodMs=period;
273
274        if (_timer!=null && (period!=old_period || _task==null))
275        {
276            synchronized (this)
277            {
278                if (_task!=null)
279                    _task.cancel();
280                _task = new TimerTask()
281                {
282                    @Override
283                    public void run()
284                    {
285                        scavenge();
286                    }
287                };
288                _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs);
289            }
290        }
291    }
292
293    /* -------------------------------------------------------------- */
294    /**
295     * Find sessions that have timed out and invalidate them. This runs in the
296     * SessionScavenger thread.
297     */
298    protected void scavenge()
299    {
300        //don't attempt to scavenge if we are shutting down
301        if (isStopping() || isStopped())
302            return;
303
304        Thread thread=Thread.currentThread();
305        ClassLoader old_loader=thread.getContextClassLoader();
306        try
307        {
308            if (_loader!=null)
309                thread.setContextClassLoader(_loader);
310
311            // For each session
312            long now=System.currentTimeMillis();
313
314            for (Iterator<HashedSession> i=_sessions.values().iterator(); i.hasNext();)
315            {
316                HashedSession session=i.next();
317                long idleTime=session.getMaxInactiveInterval()*1000L;
318                if (idleTime>0&&session.getAccessed()+idleTime<now)
319                {
320                    // Found a stale session, add it to the list
321                    try
322                    {
323                        session.timeout();
324                    }
325                    catch (Exception e)
326                    {
327                        __log.warn("Problem scavenging sessions", e);
328                    }
329                }
330                else if (_idleSavePeriodMs > 0 && session.getAccessed()+_idleSavePeriodMs < now)
331                {
332                    try
333                    {
334                        session.idle();
335                    }
336                    catch (Exception e)
337                    {
338                        __log.warn("Problem idling session "+ session.getId(), e);
339                    }
340                }
341            }
342        }
343        finally
344        {
345            thread.setContextClassLoader(old_loader);
346        }
347    }
348
349    /* ------------------------------------------------------------ */
350    @Override
351    protected void addSession(AbstractSession session)
352    {
353        if (isRunning())
354            _sessions.put(session.getClusterId(),(HashedSession)session);
355    }
356
357    /* ------------------------------------------------------------ */
358    @Override
359    public AbstractSession getSession(String idInCluster)
360    {
361        if ( _lazyLoad && !_sessionsLoaded)
362        {
363            try
364            {
365                restoreSessions();
366            }
367            catch(Exception e)
368            {
369                __log.warn(e);
370            }
371        }
372
373        Map<String,HashedSession> sessions=_sessions;
374        if (sessions==null)
375            return null;
376
377        HashedSession session = sessions.get(idInCluster);
378
379        if (session == null && _lazyLoad)
380            session=restoreSession(idInCluster);
381        if (session == null)
382            return null;
383
384        if (_idleSavePeriodMs!=0)
385            session.deIdle();
386
387        return session;
388    }
389
390    /* ------------------------------------------------------------ */
391    @Override
392    protected void invalidateSessions() throws Exception
393    {
394        // Invalidate all sessions to cause unbind events
395        ArrayList<HashedSession> sessions=new ArrayList<HashedSession>(_sessions.values());
396        int loop=100;
397        while (sessions.size()>0 && loop-->0)
398        {
399            // If we are called from doStop
400            if (isStopping() && _storeDir != null && _storeDir.exists() && _storeDir.canWrite())
401            {
402                // Then we only save and remove the session - it is not invalidated.
403                for (HashedSession session : sessions)
404                {
405                    session.save(false);
406                    removeSession(session,false);
407                }
408            }
409            else
410            {
411                for (HashedSession session : sessions)
412                    session.invalidate();
413            }
414
415            // check that no new sessions were created while we were iterating
416            sessions=new ArrayList<HashedSession>(_sessions.values());
417        }
418    }
419
420    /* ------------------------------------------------------------ */
421    @Override
422    protected AbstractSession newSession(HttpServletRequest request)
423    {
424        return new HashedSession(this, request);
425    }
426
427    /* ------------------------------------------------------------ */
428    protected AbstractSession newSession(long created, long accessed, String clusterId)
429    {
430        return new HashedSession(this, created,accessed, clusterId);
431    }
432
433    /* ------------------------------------------------------------ */
434    @Override
435    protected boolean removeSession(String clusterId)
436    {
437        return _sessions.remove(clusterId)!=null;
438    }
439
440    /* ------------------------------------------------------------ */
441    public void setStoreDirectory (File dir) throws IOException
442    {
443        // CanonicalFile is used to capture the base store directory in a way that will
444        // work on Windows.  Case differences may through off later checks using this directory.
445        _storeDir=dir.getCanonicalFile();
446    }
447
448    /* ------------------------------------------------------------ */
449    public File getStoreDirectory ()
450    {
451        return _storeDir;
452    }
453
454    /* ------------------------------------------------------------ */
455    public void setLazyLoad(boolean lazyLoad)
456    {
457        _lazyLoad = lazyLoad;
458    }
459
460    /* ------------------------------------------------------------ */
461    public boolean isLazyLoad()
462    {
463        return _lazyLoad;
464    }
465
466    /* ------------------------------------------------------------ */
467    public boolean isDeleteUnrestorableSessions()
468    {
469        return _deleteUnrestorableSessions;
470    }
471
472    /* ------------------------------------------------------------ */
473    public void setDeleteUnrestorableSessions(boolean deleteUnrestorableSessions)
474    {
475        _deleteUnrestorableSessions = deleteUnrestorableSessions;
476    }
477
478    /* ------------------------------------------------------------ */
479    public void restoreSessions () throws Exception
480    {
481        _sessionsLoaded = true;
482
483        if (_storeDir==null || !_storeDir.exists())
484        {
485            return;
486        }
487
488        if (!_storeDir.canRead())
489        {
490            __log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath());
491            return;
492        }
493
494        String[] files = _storeDir.list();
495        for (int i=0;files!=null&&i<files.length;i++)
496        {
497            restoreSession(files[i]);
498        }
499    }
500
501    /* ------------------------------------------------------------ */
502    protected synchronized HashedSession restoreSession(String idInCuster)
503    {
504        File file = new File(_storeDir,idInCuster);
505
506        FileInputStream in = null;
507        Exception error = null;
508        try
509        {
510            if (file.exists())
511            {
512                in = new FileInputStream(file);
513                HashedSession session = restoreSession(in, null);
514                addSession(session, false);
515                session.didActivate();
516                return session;
517            }
518        }
519        catch (Exception e)
520        {
521           error = e;
522        }
523        finally
524        {
525            if (in != null) IO.close(in);
526
527            if (error != null)
528            {
529                if (isDeleteUnrestorableSessions() && file.exists() && file.getParentFile().equals(_storeDir) )
530                {
531                    file.delete();
532                    __log.warn("Deleting file for unrestorable session "+idInCuster, error);
533                }
534                else
535                {
536                    __log.warn("Problem restoring session "+idInCuster, error);
537                }
538            }
539            else
540               file.delete(); //delete successfully restored file
541
542        }
543        return null;
544    }
545
546    /* ------------------------------------------------------------ */
547    public void saveSessions(boolean reactivate) throws Exception
548    {
549        if (_storeDir==null || !_storeDir.exists())
550        {
551            return;
552        }
553
554        if (!_storeDir.canWrite())
555        {
556            __log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable");
557            return;
558        }
559
560        for (HashedSession session : _sessions.values())
561            session.save(true);
562    }
563
564    /* ------------------------------------------------------------ */
565    public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception
566    {
567        /*
568         * Take care of this class's fields first by calling
569         * defaultReadObject
570         */
571        DataInputStream in = new DataInputStream(is);
572        try
573        {
574            String clusterId = in.readUTF();
575            in.readUTF(); // nodeId
576            long created = in.readLong();
577            long accessed = in.readLong();
578            int requests = in.readInt();
579
580            if (session == null)
581                session = (HashedSession)newSession(created, accessed, clusterId);
582            session.setRequests(requests);
583            int size = in.readInt();
584            if (size>0)
585            {
586                ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in);
587                try
588                {
589                    for (int i=0; i<size;i++)
590                    {
591                        String key = ois.readUTF();
592                        Object value = ois.readObject();
593                        session.setAttribute(key,value);
594                    }
595                }
596                finally
597                {
598                    IO.close(ois);
599                }
600            }
601            return session;
602        }
603        finally
604        {
605            IO.close(in);
606        }
607    }
608
609
610    /* ------------------------------------------------------------ */
611    /* ------------------------------------------------------------ */
612    protected class ClassLoadingObjectInputStream extends ObjectInputStream
613    {
614        /* ------------------------------------------------------------ */
615        public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException
616        {
617            super(in);
618        }
619
620        /* ------------------------------------------------------------ */
621        public ClassLoadingObjectInputStream () throws IOException
622        {
623            super();
624        }
625
626        /* ------------------------------------------------------------ */
627        @Override
628        public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
629        {
630            try
631            {
632                return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
633            }
634            catch (ClassNotFoundException e)
635            {
636                return super.resolveClass(cl);
637            }
638        }
639    }
640}
641