1 /**
2 * JDBM LICENSE v1.00
3 *
4 * Redistribution and use of this software and associated documentation
5 * ("Software"), with or without modification, are permitted provided
6 * that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain copyright
9 * statements and notices. Redistributions must also contain a
10 * copy of this document.
11 *
12 * 2. Redistributions in binary form must reproduce the
13 * above copyright notice, this list of conditions and the
14 * following disclaimer in the documentation and/or other
15 * materials provided with the distribution.
16 *
17 * 3. The name "JDBM" must not be used to endorse or promote
18 * products derived from this Software without prior written
19 * permission of Cees de Groot. For written permission,
20 * please contact cg@cdegroot.com.
21 *
22 * 4. Products derived from this Software may not be called "JDBM"
23 * nor may "JDBM" appear in their names without prior written
24 * permission of Cees de Groot.
25 *
26 * 5. Due credit should be given to the JDBM Project
27 * (http://jdbm.sourceforge.net/).
28 *
29 * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
30 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
31 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
32 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
33 * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
34 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
35 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
36 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
37 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
38 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
39 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
40 * OF THE POSSIBILITY OF SUCH DAMAGE.
41 *
42 * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
43 * Contributions are Copyright (C) 2000 by their associated contributors.
44 *
45 * $Id: TransactionManager.java,v 1.7 2005/06/25 23:12:32 doomdark Exp $
46 */
47
48 package jdbm.recman;
49
50 import java.io.*;
51 import java.util.*;
52
53 /**
54 * This class manages the transaction log that belongs to every
55 * {@link RecordFile}. The transaction log is either clean, or
56 * in progress. In the latter case, the transaction manager
57 * takes care of a roll forward.
58 *<p>
59 * Implementation note: this is a proof-of-concept implementation
60 * which hasn't been optimized for speed. For instance, all sorts
61 * of streams are created for every transaction.
62 */
63 // TODO: Handle the case where we are recovering lg9 and lg0, were we
64 // should start with lg9 instead of lg0!
65
66 public final class TransactionManager {
67 private RecordFile owner;
68
69 // streams for transaction log.
70 private FileOutputStream fos;
71 private ObjectOutputStream oos;
72
73 /**
74 * By default, we keep 10 transactions in the log file before
75 * synchronizing it with the main database file.
76 */
77 static final int DEFAULT_TXNS_IN_LOG = 10;
78
79 /**
80 * Maximum number of transactions before the log file is
81 * synchronized with the main database file.
82 */
83 private int _maxTxns = DEFAULT_TXNS_IN_LOG;
84
85 /**
86 * In-core copy of transactions. We could read everything back from
87 * the log file, but the RecordFile needs to keep the dirty blocks in
88 * core anyway, so we might as well point to them and spare us a lot
89 * of hassle.
90 */
91 private ArrayList[] txns = new ArrayList[DEFAULT_TXNS_IN_LOG];
92 private int curTxn = -1;
93
94 /** Extension of a log file. */
95 static final String extension = ".lg";
96
97 /**
98 * Instantiates a transaction manager instance. If recovery
99 * needs to be performed, it is done.
100 *
101 * @param owner the RecordFile instance that owns this transaction mgr.
102 */
103 TransactionManager(RecordFile owner) throws IOException {
104 this.owner = owner;
105 recover();
106 open();
107 }
108
109
110 /**
111 * Synchronize log file data with the main database file.
112 * <p>
113 * After this call, the main database file is guaranteed to be
114 * consistent and guaranteed to be the only file needed for
115 * backup purposes.
116 */
117 public void synchronizeLog()
118 throws IOException
119 {
120 synchronizeLogFromMemory();
121 }
122
123
124 /**
125 * Set the maximum number of transactions to record in
126 * the log (and keep in memory) before the log is
127 * synchronized with the main database file.
128 * <p>
129 * This method must be called while there are no
130 * pending transactions in the log.
131 */
132 public void setMaximumTransactionsInLog( int maxTxns )
133 throws IOException
134 {
135 if ( maxTxns <= 0 ) {
136 throw new IllegalArgumentException(
137 "Argument 'maxTxns' must be greater than 0." );
138 }
139 if ( curTxn != -1 ) {
140 throw new IllegalStateException(
141 "Cannot change setting while transactions are pending in the log" );
142 }
143 _maxTxns = maxTxns;
144 txns = new ArrayList[ maxTxns ];
145 }
146
147
148 /** Builds logfile name */
149 private String makeLogName() {
150 return owner.getFileName() + extension;
151 }
152
153
154 /** Synchs in-core transactions to data file and opens a fresh log */
155 private void synchronizeLogFromMemory() throws IOException {
156 close();
157
158 TreeSet blockList = new TreeSet( new BlockIoComparator() );
159
160 int numBlocks = 0;
161 int writtenBlocks = 0;
162 for (int i = 0; i < _maxTxns; i++) {
163 if (txns[i] == null)
164 continue;
165 // Add each block to the blockList, replacing the old copy of this
166 // block if necessary, thus avoiding writing the same block twice
167 for (Iterator k = txns[i].iterator(); k.hasNext(); ) {
168 BlockIo block = (BlockIo)k.next();
169 if ( blockList.contains( block ) ) {
170 block.decrementTransactionCount();
171 }
172 else {
173 writtenBlocks++;
174 boolean result = blockList.add( block );
175 }
176 numBlocks++;
177 }
178
179 txns[i] = null;
180 }
181 // Write the blocks from the blockList to disk
182 synchronizeBlocks(blockList.iterator(), true);
183
184 owner.sync();
185 open();
186 }
187
188
189 /** Opens the log file */
190 private void open() throws IOException {
191 fos = new FileOutputStream(makeLogName());
192 oos = new ObjectOutputStream(fos);
193 oos.writeShort(Magic.LOGFILE_HEADER);
194 oos.flush();
195 curTxn = -1;
196 }
197
198 /** Startup recovery on all files */
199 private void recover() throws IOException {
200 String logName = makeLogName();
201 File logFile = new File(logName);
202 if (!logFile.exists())
203 return;
204 if (logFile.length() == 0) {
205 logFile.delete();
206 return;
207 }
208
209 FileInputStream fis = new FileInputStream(logFile);
210 ObjectInputStream ois = new ObjectInputStream(fis);
211
212 try {
213 if (ois.readShort() != Magic.LOGFILE_HEADER)
214 throw new Error("Bad magic on log file");
215 } catch (IOException e) {
216 // corrupted/empty logfile
217 logFile.delete();
218 return;
219 }
220
221 while (true) {
222 ArrayList blocks = null;
223 try {
224 blocks = (ArrayList) ois.readObject();
225 } catch (ClassNotFoundException e) {
226 throw new Error("Unexcepted exception: " + e);
227 } catch (IOException e) {
228 // corrupted logfile, ignore rest of transactions
229 break;
230 }
231 synchronizeBlocks(blocks.iterator(), false);
232
233 // ObjectInputStream must match exactly each
234 // ObjectOutputStream created during writes
235 try {
236 ois = new ObjectInputStream(fis);
237 } catch (IOException e) {
238 // corrupted logfile, ignore rest of transactions
239 break;
240 }
241 }
242 owner.sync();
243 logFile.delete();
244 }
245
246 /** Synchronizes the indicated blocks with the owner. */
247 private void synchronizeBlocks(Iterator blockIterator, boolean fromCore)
248 throws IOException {
249 // write block vector elements to the data file.
250 while ( blockIterator.hasNext() ) {
251 BlockIo cur = (BlockIo)blockIterator.next();
252 owner.synch(cur);
253 if (fromCore) {
254 cur.decrementTransactionCount();
255 if (!cur.isInTransaction()) {
256 owner.releaseFromTransaction(cur, true);
257 }
258 }
259 }
260 }
261
262
263 /** Set clean flag on the blocks. */
264 private void setClean(ArrayList blocks)
265 throws IOException {
266 for (Iterator k = blocks.iterator(); k.hasNext(); ) {
267 BlockIo cur = (BlockIo) k.next();
268 cur.setClean();
269 }
270 }
271
272 /** Discards the indicated blocks and notify the owner. */
273 private void discardBlocks(ArrayList blocks)
274 throws IOException {
275 for (Iterator k = blocks.iterator(); k.hasNext(); ) {
276 BlockIo cur = (BlockIo) k.next();
277 cur.decrementTransactionCount();
278 if (!cur.isInTransaction()) {
279 owner.releaseFromTransaction(cur, false);
280 }
281 }
282 }
283
284 /**
285 * Starts a transaction. This can block if all slots have been filled
286 * with full transactions, waiting for the synchronization thread to
287 * clean out slots.
288 */
289 void start() throws IOException {
290 curTxn++;
291 if (curTxn == _maxTxns) {
292 synchronizeLogFromMemory();
293 curTxn = 0;
294 }
295 txns[curTxn] = new ArrayList();
296 }
297
298 /**
299 * Indicates the block is part of the transaction.
300 */
301 void add(BlockIo block) throws IOException {
302 block.incrementTransactionCount();
303 txns[curTxn].add(block);
304 }
305
306 /**
307 * Commits the transaction to the log file.
308 */
309 void commit() throws IOException {
310 oos.writeObject(txns[curTxn]);
311 sync();
312
313 // set clean flag to indicate blocks have been written to log
314 setClean(txns[curTxn]);
315
316 // open a new ObjectOutputStream in order to store
317 // newer states of BlockIo
318 oos = new ObjectOutputStream(fos);
319 }
320
321 /** Flushes and syncs */
322 private void sync() throws IOException {
323 oos.flush();
324 fos.flush();
325 fos.getFD().sync();
326 }
327
328 /**
329 * Shutdowns the transaction manager. Resynchronizes outstanding
330 * logs.
331 */
332 void shutdown() throws IOException {
333 synchronizeLogFromMemory();
334 close();
335 }
336
337 /**
338 * Closes open files.
339 */
340 private void close() throws IOException {
341 sync();
342 oos.close();
343 fos.close();
344 oos = null;
345 fos = null;
346 }
347
348 /**
349 * Force closing the file without synchronizing pending transaction data.
350 * Used for testing purposes only.
351 */
352 void forceClose() throws IOException {
353 oos.close();
354 fos.close();
355 oos = null;
356 fos = null;
357 }
358
359 /**
360 * Use the disk-based transaction log to synchronize the data file.
361 * Outstanding memory logs are discarded because they are believed
362 * to be inconsistent.
363 */
364 void synchronizeLogFromDisk() throws IOException {
365 close();
366
367 for ( int i=0; i < _maxTxns; i++ ) {
368 if (txns[i] == null)
369 continue;
370 discardBlocks(txns[i]);
371 txns[i] = null;
372 }
373
374 recover();
375 open();
376 }
377
378
379 /** INNER CLASS.
380 * Comparator class for use by the tree set used to store the blocks
381 * to write for this transaction. The BlockIo objects are ordered by
382 * their blockIds.
383 */
384 public static class BlockIoComparator
385 implements Comparator
386 {
387
388 public int compare( Object o1, Object o2 ) {
389 BlockIo block1 = (BlockIo)o1;
390 BlockIo block2 = (BlockIo)o2;
391 int result = 0;
392 if ( block1.getBlockId() == block2.getBlockId() ) {
393 result = 0;
394 }
395 else if ( block1.getBlockId() < block2.getBlockId() ) {
396 result = -1;
397 }
398 else {
399 result = 1;
400 }
401 return result;
402 }
403
404 public boolean equals(Object obj) {
405 return super.equals(obj);
406 }
407 } // class BlockIOComparator
408
409 }