1 /***
2 *
3 * Copyright 2004 Hiram Chirino
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 **/
18 package org.activeio.journal.active;
19
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InterruptedIOException;
23 import java.lang.reflect.InvocationTargetException;
24
25 import org.activeio.Disposable;
26 import org.activeio.Packet;
27 import org.activeio.journal.InvalidRecordLocationException;
28 import org.activeio.journal.Journal;
29 import org.activeio.journal.JournalEventListener;
30 import org.activeio.journal.RecordLocation;
31 import org.activeio.packet.ByteArrayPacket;
32 import org.activeio.packet.ByteBufferPacketPool;
33
34 import EDU.oswego.cs.dl.util.concurrent.FutureResult;
35 import EDU.oswego.cs.dl.util.concurrent.QueuedExecutor;
36 import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;
37
38 /***
39 * A high speed Journal implementation. Inspired by the ideas of the <a
40 * href="http://howl.objectweb.org/">Howl </a> project but tailored to the needs
41 * of ActiveMQ. <p/>This Journal provides the following features:
42 * <ul>
43 * <li>Conncurrent writes are batched into a single write/force done by a
44 * background thread.</li>
45 * <li>Uses preallocated logs to avoid disk fragmentation and performance
46 * degregation.</li>
47 * <li>The number and size of the preallocated logs are configurable.</li>
48 * <li>Uses direct ByteBuffers to write data to log files.</li>
49 * <li>Allows logs to grow in case of an overflow condition so that overflow
50 * exceptions are not not thrown. Grown logs that are inactivated (due to a new
51 * mark) are resized to thier original size.</li>
52 * <li>No limit on the size of the record written to the journal</li>
53 * <li>Should be possible to extend so that multiple physical disk are used
54 * concurrently to increase throughput and decrease latency.</li>
55 * </ul>
56 * <p/>
57 *
58 * @version $Revision: 1.1 $
59 */
60 final public class JournalImpl implements Journal, Disposable {
61
62 public static final int DEFAULT_POOL_SIZE = Integer.parseInt(System.getProperty("org.activeio.journal.active.DefaultPoolSize", ""+(5)));
63 public static final int DEFAULT_PACKET_SIZE = Integer.parseInt(System.getProperty("org.activeio.journal.active.DefaultPacketSize", ""+(1024*1024*4)));
64
65 static final private int OVERFLOW_RENOTIFICATION_DELAY = 500;
66
67 private boolean disposed = false;
68
69
70 private int appendLogFileId = 0;
71
72
73 private int appendLogFileOffset = 0;
74
75
76 private BatchedWrite pendingBatchWrite;
77
78 private Location lastMarkedLocation;
79 private LogFileManager file;
80 private QueuedExecutor executor;
81 private int rolloverFence;
82 private JournalEventListener eventListener;
83 private ByteBufferPacketPool packetPool;
84 private long overflowNotificationTime = System.currentTimeMillis();
85 private Packet markPacket = new ByteArrayPacket(new byte[Location.SERIALIZED_SIZE]);
86
87 public JournalImpl(File logDirectory) throws IOException {
88 this(new LogFileManager(logDirectory));
89 }
90
91 public JournalImpl(File logDirectory, int logFileCount, int logFileSize) throws IOException {
92 this(new LogFileManager(logDirectory, logFileCount, logFileSize));
93 }
94
95 public JournalImpl(LogFileManager logFile) {
96 this.file = logFile;
97 this.packetPool = new ByteBufferPacketPool(DEFAULT_POOL_SIZE, DEFAULT_PACKET_SIZE);
98 this.executor = new QueuedExecutor();
99 this.executor.setThreadFactory(new ThreadFactory() {
100 public Thread newThread(Runnable runnable) {
101 Thread answer = new Thread(runnable, "Journal Writter");
102 answer.setPriority(Thread.MAX_PRIORITY);
103 answer.setDaemon(true);
104 return answer;
105 }
106 });
107
108 lastMarkedLocation = file.getLastMarkedRecordLocation();
109 Location nextAppendLocation = file.getNextAppendLocation();
110 appendLogFileId = nextAppendLocation.getLogFileId();
111 appendLogFileOffset = nextAppendLocation.getLogFileOffset();
112
113 rolloverFence = (file.getInitialLogFileSize() / 10) * 9;
114 }
115
116 public RecordLocation write(Packet data, boolean sync) throws IOException {
117 return write(LogFileManager.DATA_RECORD_TYPE, data, sync, null);
118 }
119
120 private Location write(byte recordType, Packet data, boolean sync, Location mark) throws IOException {
121 try {
122 Location location;
123 BatchedWrite writeCommand;
124
125 Record record = new Record(recordType, data, mark);
126
127
128
129 synchronized (this) {
130 if (disposed) {
131 throw new IOException("Journal has been closed.");
132 }
133
134
135 location = new Location(appendLogFileId, appendLogFileOffset);
136 record.setLocation(location);
137
138
139 writeCommand = addToPendingWriteBatch(record, mark, sync);
140
141
142 appendLogFileOffset += data.limit() + Record.RECORD_BASE_SIZE;
143 rolloverCheck();
144 }
145
146 if (sync) {
147 writeCommand.waitForForce();
148 }
149
150 return location;
151 } catch (IOException e) {
152 throw e;
153 } catch (InterruptedException e) {
154 throw (IOException) new InterruptedIOException().initCause(e);
155 } catch (Throwable e) {
156 throw (IOException) new IOException("Write failed: " + e).initCause(e);
157 }
158 }
159
160 /***
161 * @param record
162 * @return
163 * @throws InterruptedException
164 */
165 private BatchedWrite addToPendingWriteBatch(Record record, Location mark, boolean force) throws InterruptedException {
166
167
168
169 BatchedWrite answer = null;
170 while (record.hasRemaining()) {
171
172
173 boolean queueTheWrite=false;
174 if (pendingBatchWrite == null) {
175 pendingBatchWrite = new BatchedWrite(packetPool.getPacket());
176 queueTheWrite = true;
177 }
178 answer = pendingBatchWrite;
179
180
181 boolean full = !pendingBatchWrite.append(record, mark, force);
182
183 if( queueTheWrite ) {
184 final BatchedWrite queuedWrite = pendingBatchWrite;
185 executor.execute(new Runnable() {
186 public void run() {
187 try {
188 queuedWrite(queuedWrite);
189 } catch (InterruptedException e) {
190 }
191 }
192 });
193 }
194
195 if( full )
196 pendingBatchWrite = null;
197 }
198 return answer;
199
200 }
201
202 /***
203 * This is a blocking call
204 *
205 * @param write
206 * @throws InterruptedException
207 */
208 private void queuedWrite(BatchedWrite write) throws InterruptedException {
209
210
211 write.flip();
212
213
214 try {
215 file.append(write);
216 write.forced();
217 } catch (Throwable e) {
218 write.writeFailed(e);
219 } finally {
220 write.getPacket().dispose();
221 }
222 }
223
224 /***
225 *
226 */
227 private void rolloverCheck() throws IOException {
228
229
230 if (eventListener != null && file.isPastHalfActive()
231 && overflowNotificationTime + OVERFLOW_RENOTIFICATION_DELAY < System.currentTimeMillis()) {
232
233
234
235 Location safeSpot = file.getFirstRecordLocationOfSecondActiveLogFile();
236 eventListener.overflowNotification(safeSpot);
237 overflowNotificationTime = System.currentTimeMillis();
238 }
239
240
241 if (appendLogFileOffset > rolloverFence ) {
242
243
244 if ( !file.canActivateNextLogFile() ) {
245
246 overflowNotificationTime -= OVERFLOW_RENOTIFICATION_DELAY;
247
248 } else {
249
250 final FutureResult result = new FutureResult();
251 try {
252 executor.execute(new Runnable() {
253 public void run() {
254 try {
255 result.set(queuedActivateNextLogFile());
256 } catch (Throwable e) {
257 result.setException(e);
258 }
259 }
260 });
261
262 Location location = (Location) result.get();
263 appendLogFileId = location.getLogFileId();
264 appendLogFileOffset = location.getLogFileOffset();
265
266 } catch (InterruptedException e) {
267 throw (IOException) new IOException("Interrupted.").initCause(e);
268 } catch (InvocationTargetException e) {
269 if (e.getTargetException() instanceof IOException)
270 throw (IOException) new IOException(e.getTargetException().getMessage()).initCause(e
271 .getTargetException());
272 throw (IOException) new IOException("Unexpected Exception: ").initCause(e.getTargetException());
273 }
274 }
275 }
276 }
277
278 /***
279 * This is a blocking call
280 */
281 private Location queuedActivateNextLogFile() throws IOException {
282 file.activateNextLogFile();
283 return file.getNextAppendLocation();
284 }
285
286
287
288 /***
289 * @param recordLocator
290 * @param force
291 * @return
292 * @throws InvalidRecordLocationException
293 * @throws IOException
294 * @throws InterruptedException
295 */
296 synchronized public void setMark(RecordLocation l, boolean force) throws InvalidRecordLocationException,
297 IOException {
298
299 Location location = (Location) l;
300 if (location == null)
301 throw new InvalidRecordLocationException("The location cannot be null.");
302 if (lastMarkedLocation != null && location.compareTo(lastMarkedLocation) < 0)
303 throw new InvalidRecordLocationException("The location is less than the last mark.");
304
305 markPacket.clear();
306 location.writeToPacket(markPacket);
307 markPacket.flip();
308 write(LogFileManager.MARK_RECORD_TYPE, markPacket, force, location);
309
310 lastMarkedLocation = location;
311 }
312
313 /***
314 * @return
315 */
316 public RecordLocation getMark() {
317 return lastMarkedLocation;
318 }
319
320 /***
321 * @param lastLocation
322 * @return
323 * @throws IOException
324 * @throws InvalidRecordLocationException
325 */
326 public RecordLocation getNextRecordLocation(final RecordLocation lastLocation) throws IOException,
327 InvalidRecordLocationException {
328
329 if (lastLocation == null) {
330 if (lastMarkedLocation != null) {
331 return lastMarkedLocation;
332 } else {
333 return file.getFirstActiveLogLocation();
334 }
335 }
336
337
338 final FutureResult result = new FutureResult();
339 try {
340 executor.execute(new Runnable() {
341 public void run() {
342 try {
343 result.set(queuedGetNextRecordLocation((Location) lastLocation));
344 } catch (Throwable e) {
345 result.setException(e);
346 }
347 }
348 });
349 return (Location) result.get();
350 } catch (InterruptedException e) {
351 throw (IOException) new IOException("Interrupted.").initCause(e);
352 } catch (InvocationTargetException e) {
353 return (RecordLocation) unwrapException(e);
354
355 }
356 }
357
358 private Object unwrapException(InvocationTargetException e) throws InvalidRecordLocationException, IOException {
359 if (e.getTargetException() instanceof InvalidRecordLocationException)
360 throw new InvalidRecordLocationException(e.getTargetException().getMessage(), e.getTargetException());
361 if (e.getTargetException() instanceof IOException)
362 throw (IOException) new IOException(e.getTargetException().getMessage()).initCause(e
363 .getTargetException());
364 throw (IOException) new IOException("Unexpected Exception: ").initCause(e.getTargetException());
365 }
366
367 private Location queuedGetNextRecordLocation(Location location) throws IOException, InvalidRecordLocationException {
368 return file.getNextDataRecordLocation(location);
369 }
370
371 /***
372 * @param location
373 * @return
374 * @throws InvalidRecordLocationException
375 * @throws IOException
376 */
377 public Packet read(final RecordLocation l) throws IOException, InvalidRecordLocationException {
378 final Location location = (Location) l;
379
380 final FutureResult result = new FutureResult();
381 try {
382 executor.execute(new Runnable() {
383 public void run() {
384 try {
385 result.set(file.readPacket(location));
386 } catch (Throwable e) {
387 result.setException(e);
388 }
389 }
390 });
391 return (Packet) result.get();
392 } catch (InterruptedException e) {
393 throw (IOException) new IOException("Interrupted.").initCause(e);
394 } catch (InvocationTargetException e) {
395 if (e.getTargetException() instanceof InvalidRecordLocationException)
396 throw new InvalidRecordLocationException(e.getTargetException().getMessage(), e.getTargetException());
397 if (e.getTargetException() instanceof IOException)
398 throw (IOException) new IOException(e.getTargetException().getMessage()).initCause(e
399 .getTargetException());
400 throw (IOException) new IOException("Unexpected Exception: ").initCause(e.getTargetException());
401 }
402 }
403
404 public void setJournalEventListener(JournalEventListener eventListener) {
405 this.eventListener = eventListener;
406 }
407
408 /***
409 * @deprecated @see #dispose()
410 */
411 public void close() throws IOException {
412 dispose();
413 }
414
415 /***
416 */
417 public void dispose() {
418 if (disposed)
419 return;
420 disposed=true;
421 executor.shutdownAfterProcessingCurrentlyQueuedTasks();
422 file.dispose();
423 }
424
425 /***
426 * @return
427 */
428 public File getLogDirectory() {
429 return file.getLogDirectory();
430 }
431
432 public int getInitialLogFileSize() {
433 return file.getInitialLogFileSize();
434 }
435
436 public String toString() {
437 return "Active Journal: using "+file.getOnlineLogFileCount()+" x " + (file.getInitialLogFileSize()/(1024*1024f)) + " Megs at: " + getLogDirectory();
438 }
439
440 }