1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *      http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2013 - 2016
18   *
19   */
20  package org.crosswire.jsword.book.sword.state;
21  
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.Map;
25  import java.util.Queue;
26  import java.util.concurrent.ConcurrentLinkedQueue;
27  import java.util.concurrent.Executors;
28  import java.util.concurrent.ScheduledFuture;
29  import java.util.concurrent.ThreadFactory;
30  import java.util.concurrent.TimeUnit;
31  
32  import org.crosswire.jsword.book.BookException;
33  import org.crosswire.jsword.book.BookMetaData;
34  import org.crosswire.jsword.book.sword.BlockType;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  /**
39   * Manages the creation and re-distribution of open file states. This increases
40   * performance as more often than not, the same file state may be used. For
41   * example we may be carrying out a contains() operation followed by a read to
42   * disk for a particular key
43   * 
44   * Each {@link BookMetaData} has a corresponding a file state which is
45   * different to another. Furthermore, concurrent accesses cannot share this file
46   * state as the {@link OpenFileState} records where in the file it is, for
47   * reading several verses together for example. As a result, we want to key a
48   * lookup by {@link BookMetaData}, which then gives us a pool of available
49   * file states... We create some more if none are available.
50   * 
51   * In order to prevent memory leaks (OpenFileStates might be quite heavy as they do some internal caching of file data..
52   * In order to avoid many file references piling up in memory, we implement a background cleaning thread which will clean
53   * up redundant keys every so often.
54   *
55   *
56   * 
57   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
58   * 
59   * @author DM Smith
60   * @author Chris Burrell
61   */
62  public final class OpenFileStateManager {
63      /**
64       * prevent instantiation
65       */
66      private OpenFileStateManager(final int cleanupIntervalSeconds, final int maxExpiry) {
67          // no op
68          this.monitoringThread = Executors.newScheduledThreadPool(1, new ThreadFactory() {
69              public Thread newThread(Runnable r) {
70                  Thread t = new Thread(r);
71                  t.setDaemon(true);
72                  return t;
73              }
74  
75          }).scheduleWithFixedDelay(new Runnable() {
76              public void run() {
77                  // check the state of the maps and queues... The queues may have too much in them and that will in turn max out
78                  // the heap.
79                  long currentTime = System.currentTimeMillis();
80  
81                  for (Queue<OpenFileState> e : OpenFileStateManager.this.metaToStates.values()) {
82                      for (Iterator<OpenFileState> iterator = e.iterator(); iterator.hasNext(); ) {
83                          final OpenFileState state = iterator.next();
84                          if (state.getLastAccess() + maxExpiry * 1000 < currentTime) {
85                              //release resources
86                              state.releaseResources();
87  
88                              //remove from the queues
89                              iterator.remove();
90                          }
91                      }
92                  }
93              }
94          }, 0, cleanupIntervalSeconds, TimeUnit.SECONDS);
95      }
96  
97      /**
98       * Allow the caller to initialize with their own settings. Should the OpenFileStateManager already be initialized
99       * a no-op will occur. No need for double-checked locking here
100      * 
101      * @param cleanupIntervalSeconds seconds before cleanup
102      * @param maxExpiry 
103      */
104     public static synchronized void init(final int cleanupIntervalSeconds, final int maxExpiry) {
105         if (manager == null) {
106             manager = new OpenFileStateManager(cleanupIntervalSeconds, maxExpiry);
107         } else {
108             // already initialized
109             LOGGER.warn("The OpenFileStateManager has already been initialised, potentially with its default settings. The following values were ignored: cleanUpInterval [{}], maxExpiry=[{}]", Integer.toString(cleanupIntervalSeconds), Integer.toString(maxExpiry));
110         }
111 
112     }
113 
114     /**
115      * Singleton instance method to return the one and only Open File State Manager
116      * @return the singleton
117      */
118     public static OpenFileStateManager instance() {
119         if (manager == null) {
120             synchronized (OpenFileStateManager.class) {
121                 init(60, 60);
122             }
123         }
124         return manager;
125     }
126 
127     public RawBackendState getRawBackendState(BookMetaData metadata) throws BookException {
128         ensureNotShuttingDown();
129 
130         RawBackendState state = getInstance(metadata);
131         if (state == null) {
132             LOGGER.trace("Initializing: {}", metadata.getInitials());
133             return new RawBackendState(metadata);
134         }
135 
136         LOGGER.trace("Reusing: {}", metadata.getInitials());
137         return state;
138     }
139 
140     public RawFileBackendState getRawFileBackendState(BookMetaData metadata) throws BookException {
141         ensureNotShuttingDown();
142 
143         RawFileBackendState state = getInstance(metadata);
144         if (state == null) {
145             LOGGER.trace("Initializing: {}", metadata.getInitials());
146             return new RawFileBackendState(metadata);
147         }
148 
149         LOGGER.trace("Reusing: {}", metadata.getInitials());
150         return state;
151     }
152 
153     public GenBookBackendState getGenBookBackendState(BookMetaData metadata) throws BookException {
154         ensureNotShuttingDown();
155 
156         GenBookBackendState state = getInstance(metadata);
157         if (state == null) {
158             LOGGER.trace("Initializing: {}", metadata.getInitials());
159             return new GenBookBackendState(metadata);
160         }
161 
162         LOGGER.trace("Reusing: {}", metadata.getInitials());
163         return state;
164     }
165 
166     public RawLDBackendState getRawLDBackendState(BookMetaData metadata) throws BookException {
167         ensureNotShuttingDown();
168 
169         RawLDBackendState state = getInstance(metadata);
170         if (state == null) {
171             LOGGER.trace("Initializing: {}", metadata.getInitials());
172             return new RawLDBackendState(metadata);
173         }
174 
175         LOGGER.trace("Reusing: {}", metadata.getInitials());
176         return state;
177     }
178 
179     public ZLDBackendState getZLDBackendState(BookMetaData metadata) throws BookException {
180         ensureNotShuttingDown();
181 
182         ZLDBackendState state = getInstance(metadata);
183         if (state == null) {
184             LOGGER.trace("Initializing: {}", metadata.getInitials());
185             return new ZLDBackendState(metadata);
186         }
187 
188         LOGGER.trace("Reusing: {}", metadata.getInitials());
189         return state;
190     }
191 
192     public ZVerseBackendState getZVerseBackendState(BookMetaData metadata, BlockType blockType) throws BookException {
193         ensureNotShuttingDown();
194 
195         ZVerseBackendState state = getInstance(metadata);
196         if (state == null) {
197             LOGGER.trace("Initializing: {}", metadata.getInitials());
198             return new ZVerseBackendState(metadata, blockType);
199         }
200 
201         LOGGER.trace("Reusing: {}", metadata.getInitials());
202         return state;
203     }
204 
205     @SuppressWarnings("unchecked")
206     private <T extends OpenFileState> T getInstance(BookMetaData metadata) {
207         Queue<OpenFileState> availableStates = getQueueForMeta(metadata);
208         final T state = (T) availableStates.poll();
209 
210         //while not strictly necessary, the documentation suggests that iterating through the collection
211         //gives you a snapshot at some point in time, though not necessarily consistent, so just in case this remains
212         //in access of the iterator() functionality, we update the last access date to avoid it being destroyed while we
213         //use it
214         if (state != null) {
215             state.setLastAccess(System.currentTimeMillis());
216         }
217         return state;
218     }
219 
220     private Queue<OpenFileState> getQueueForMeta(BookMetaData metadata) {
221         Queue<OpenFileState> availableStates = metaToStates.get(metadata);
222         if (availableStates == null) {
223             synchronized (OpenFileState.class) {
224                 availableStates = new ConcurrentLinkedQueue<OpenFileState>();
225                 metaToStates.put(metadata, availableStates);
226             }
227         }
228         return availableStates;
229     }
230 
231     public void release(OpenFileState fileState) {
232         if (fileState == null) {
233             // can't release anything. JSword has failed to open a file state,
234             // and a finally block is trying to close this
235             return;
236         }
237 
238         fileState.setLastAccess(System.currentTimeMillis());
239 
240         // instead of releasing, we add to our queue
241         BookMetaData bmd = fileState.getBookMetaData();
242         Queue<OpenFileState> queueForMeta = getQueueForMeta(bmd);
243         LOGGER.trace("Offering to releasing: {}", bmd.getInitials());
244         boolean offered = queueForMeta.offer(fileState);
245 
246         // ignore if we couldn't offer to the queue
247         if (!offered) {
248             LOGGER.trace("Released: {}", bmd.getInitials());
249             fileState.releaseResources();
250         }
251     }
252 
253     /**
254      * Shuts down all open files
255      */
256     public void shutDown() {
257         shuttingDown = true;
258         this.monitoringThread.cancel(true);
259         for (Queue<OpenFileState> e : metaToStates.values()) {
260             OpenFileState state = null;
261             while ((state = e.poll()) != null) {
262                 state.releaseResources();
263             }
264         }
265     }
266 
267     private void ensureNotShuttingDown() throws BookException {
268         if (shuttingDown) {
269             throw new BookException("Unable to read book, application is shutting down.");
270         }
271     }
272 
273     private final ScheduledFuture<?> monitoringThread;
274     private final Map<BookMetaData, Queue<OpenFileState>> metaToStates = new HashMap<BookMetaData, Queue<OpenFileState>>();
275     private volatile boolean shuttingDown;
276 
277     private static volatile OpenFileStateManager manager;
278     private static final Logger LOGGER = LoggerFactory.getLogger(OpenFileStateManager.class);
279 }
280