Skip to content

Commit d13dc20

Browse files
billestiborpalacsint
authored andcommitted
Implements interrupt-safe transaction log
When an interrupt happened on a thread during writing the transaction log, the underlying FileChannel was closed as a result. Any further use (even from another threads) threw a java.nio.channels.ClosedChannelException rendering the transaction manager unusable since the transaction logs were never reopened. This commit changes this behavior, log file operations reopen the file if it was closed by an interrupt of another thread. fixes scalar-labs#45 Commit by Tibor Billes, Miklós Karakó, Balázs Póka
1 parent 1072c30 commit d13dc20

11 files changed

+873
-41
lines changed

btm/pom.xml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<packaging>bundle</packaging>
1313

1414
<dependencies>
15-
<dependency>
15+
<dependency>
1616
<groupId>javax.transaction</groupId>
1717
<artifactId>jta</artifactId>
1818
<scope>provided</scope>
@@ -70,6 +70,24 @@
7070
<scope>provided</scope>
7171
<optional>true</optional>
7272
</dependency>
73+
<dependency>
74+
<groupId>org.apache.commons</groupId>
75+
<artifactId>commons-io</artifactId>
76+
<version>1.3.2</version>
77+
<scope>test</scope>
78+
</dependency>
79+
<dependency>
80+
<groupId>org.apache.commons</groupId>
81+
<artifactId>commons-lang3</artifactId>
82+
<version>3.4</version>
83+
<scope>test</scope>
84+
</dependency>
85+
<dependency>
86+
<groupId>org.easytesting</groupId>
87+
<artifactId>fest-assert-core</artifactId>
88+
<version>2.0M10</version>
89+
<scope>test</scope>
90+
</dependency>
7391
</dependencies>
7492

7593
<build>

btm/src/main/java/bitronix/tm/gui/TransactionLogHeaderPanel.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package bitronix.tm.gui;
1717

1818
import bitronix.tm.journal.TransactionLogHeader;
19+
import bitronix.tm.journal.InterruptibleLockedRandomAccessFile;
1920
import bitronix.tm.utils.Decoder;
2021
import org.slf4j.Logger;
2122
import org.slf4j.LoggerFactory;
@@ -74,8 +75,8 @@ public void setPosition(long position) {
7475
}
7576

7677
public void read(File logFile, boolean active) throws IOException {
77-
RandomAccessFile raf = new RandomAccessFile(logFile, "r");
78-
TransactionLogHeader header = new TransactionLogHeader(raf.getChannel(), 0L);
78+
InterruptibleLockedRandomAccessFile raf = new InterruptibleLockedRandomAccessFile(logFile, "r");
79+
TransactionLogHeader header = new TransactionLogHeader(raf, 0L);
7980
raf.close();
8081
if (log.isDebugEnabled()) { log.debug("read header: " + header); }
8182
setLogFile(logFile);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package bitronix.tm.journal;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.io.RandomAccessFile;
6+
import java.nio.ByteBuffer;
7+
import java.nio.channels.FileChannel;
8+
import java.nio.channels.FileLock;
9+
10+
public class InterruptibleLockedRandomAccessFile {
11+
12+
private final File file;
13+
private final String mode;
14+
private RandomAccessFile openedFile;
15+
private FileChannel fileChannel;
16+
private FileLock fileLock;
17+
private long currentPosition = 0;
18+
private boolean closed;
19+
20+
public InterruptibleLockedRandomAccessFile(final File file, final String mode)
21+
throws IOException {
22+
this.file = file;
23+
this.mode = mode;
24+
open();
25+
}
26+
27+
private synchronized void open() throws IOException {
28+
openedFile = new RandomAccessFile(file, mode);
29+
fileChannel = openedFile.getChannel();
30+
31+
final boolean shared = false;
32+
this.fileLock = fileChannel
33+
.tryLock(0, TransactionLogHeader.TIMESTAMP_HEADER, shared);
34+
if (this.fileLock == null) {
35+
throw new IOException("File " + file.getAbsolutePath()
36+
+ " is locked. Is another instance already running?");
37+
}
38+
}
39+
40+
public synchronized final void close() throws IOException {
41+
try {
42+
if (!fileLock.isValid()) {
43+
checkState(!fileChannel.isOpen(), "invalid/unhandled state");
44+
return;
45+
}
46+
fileLock.release();
47+
fileChannel.close();
48+
openedFile.close();
49+
} finally {
50+
closed = true;
51+
}
52+
}
53+
54+
public synchronized void position(final long newPosition) throws IOException {
55+
checkNotClosed();
56+
reopenFileChannelIfClosed();
57+
58+
fileChannel.position(newPosition);
59+
currentPosition = newPosition;
60+
}
61+
62+
private void checkNotClosed() {
63+
checkState(!closed, "File has been closed");
64+
}
65+
66+
private static void checkState(final boolean expression, final String errorMessage) {
67+
if (!expression) {
68+
throw new IllegalStateException(errorMessage);
69+
}
70+
}
71+
72+
public synchronized void force(final boolean metaData) throws IOException {
73+
checkNotClosed();
74+
reopenFileChannelIfClosed();
75+
76+
fileChannel.force(metaData);
77+
}
78+
79+
public synchronized int write(final ByteBuffer src, final long position)
80+
throws IOException {
81+
checkNotClosed();
82+
reopenFileChannelIfClosed();
83+
84+
return fileChannel.write(src, position);
85+
}
86+
87+
public synchronized void read(final ByteBuffer buffer) throws IOException {
88+
checkNotClosed();
89+
reopenFileChannelIfClosed();
90+
91+
fileChannel.read(buffer);
92+
currentPosition = fileChannel.position();
93+
}
94+
95+
private void reopenFileChannelIfClosed() throws IOException {
96+
if (!fileChannel.isOpen()) {
97+
open();
98+
}
99+
100+
if (fileChannel.position() != currentPosition) {
101+
fileChannel.position(currentPosition);
102+
}
103+
}
104+
}

btm/src/main/java/bitronix/tm/journal/TransactionLogAppender.java

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,9 @@
1515
*/
1616
package bitronix.tm.journal;
1717

18-
import bitronix.tm.utils.Uid;
19-
import org.slf4j.Logger;
20-
import org.slf4j.LoggerFactory;
21-
22-
import javax.transaction.Status;
2318
import java.io.File;
2419
import java.io.IOException;
25-
import java.io.RandomAccessFile;
2620
import java.nio.ByteBuffer;
27-
import java.nio.channels.FileChannel;
28-
import java.nio.channels.FileLock;
2921
import java.util.ArrayList;
3022
import java.util.Collections;
3123
import java.util.Comparator;
@@ -35,6 +27,13 @@
3527
import java.util.TreeSet;
3628
import java.util.concurrent.atomic.AtomicInteger;
3729

30+
import javax.transaction.Status;
31+
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
import bitronix.tm.utils.Uid;
36+
3837
/**
3938
* Used to write {@link TransactionLogRecord} objects to a log file.
4039
*
@@ -53,9 +52,7 @@ public class TransactionLogAppender {
5352
public static final int END_RECORD = 0x786e7442;
5453

5554
private final File file;
56-
private final RandomAccessFile randomeAccessFile;
57-
private final FileChannel fc;
58-
private final FileLock lock;
55+
private final InterruptibleLockedRandomAccessFile randomAccessFile;
5956
private final TransactionLogHeader header;
6057
private final long maxFileLength;
6158
private final AtomicInteger outstandingWrites;
@@ -70,14 +67,10 @@ public class TransactionLogAppender {
7067
*/
7168
public TransactionLogAppender(File file, long maxFileLength) throws IOException {
7269
this.file = file;
73-
this.randomeAccessFile = new RandomAccessFile(file, "rw");
74-
this.fc = randomeAccessFile.getChannel();
75-
this.header = new TransactionLogHeader(fc, maxFileLength);
70+
this.randomAccessFile = new InterruptibleLockedRandomAccessFile(file, "rw");
71+
this.header = new TransactionLogHeader(randomAccessFile, maxFileLength);
7672
this.maxFileLength = maxFileLength;
77-
this.lock = fc.tryLock(0, TransactionLogHeader.TIMESTAMP_HEADER, false);
78-
if (this.lock == null)
79-
throw new IOException("transaction log file " + file.getName() + " is locked. Is another instance already running?");
80-
73+
8174
this.outstandingWrites = new AtomicInteger();
8275

8376
this.danglingRecords = new HashMap<Uid, Set<String>>();
@@ -144,7 +137,7 @@ protected void writeLog(TransactionLogRecord tlog) throws IOException {
144137

145138
final long writePosition = tlog.getWritePosition();
146139
while (buf.hasRemaining()) {
147-
fc.write(buf, writePosition + buf.position());
140+
randomAccessFile.write(buf, writePosition + buf.position());
148141
}
149142

150143
trackOutstanding(status, gtrid, uniqueNames);
@@ -273,18 +266,14 @@ public long getPosition() {
273266
return position;
274267
}
275268

276-
277269
/**
278270
* Close the appender and the underlying file.
279271
* @throws IOException if an I/O error occurs.
280272
*/
281273
protected void close() throws IOException {
282274
header.setState(TransactionLogHeader.CLEAN_LOG_STATE);
283-
fc.force(false);
284-
if (lock != null)
285-
lock.release();
286-
fc.close();
287-
randomeAccessFile.close();
275+
randomAccessFile.force(false);
276+
randomAccessFile.close();
288277
}
289278

290279
/**
@@ -304,7 +293,7 @@ protected TransactionLogCursor getCursor() throws IOException {
304293
*/
305294
protected void force() throws IOException {
306295
if (log.isDebugEnabled()) { log.debug("forcing log writing"); }
307-
fc.force(false);
296+
randomAccessFile.force(false);
308297
if (log.isDebugEnabled()) { log.debug("done forcing log"); }
309298
}
310299

btm/src/main/java/bitronix/tm/journal/TransactionLogHeader.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public class TransactionLogHeader {
6969
*/
7070
public final static byte UNCLEAN_LOG_STATE = -1;
7171

72-
private final FileChannel fc;
72+
private final InterruptibleLockedRandomAccessFile file;
7373
private final long maxFileLength;
7474

7575
private volatile int formatId;
@@ -79,25 +79,25 @@ public class TransactionLogHeader {
7979

8080
/**
8181
* TransactionLogHeader are used to control headers of the specified RandomAccessFile.
82-
* @param fc the file channel to read from.
82+
* @param randomAccessFile the file to read from.
8383
* @param maxFileLength the max file length.
8484
* @throws IOException if an I/O error occurs.
8585
*/
86-
public TransactionLogHeader(FileChannel fc, long maxFileLength) throws IOException {
87-
this.fc = fc;
86+
public TransactionLogHeader(InterruptibleLockedRandomAccessFile randomAccessFile, long maxFileLength) throws IOException {
87+
this.file = randomAccessFile;
8888
this.maxFileLength = maxFileLength;
8989

90-
fc.position(FORMAT_ID_HEADER);
90+
randomAccessFile.position(FORMAT_ID_HEADER);
9191
ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 1 + 8);
9292
while (buf.hasRemaining()) {
93-
this.fc.read(buf);
93+
this.file.read(buf);
9494
}
9595
buf.flip();
9696
formatId = buf.getInt();
9797
timestamp = buf.getLong();
9898
state = buf.get();
9999
position = buf.getLong();
100-
fc.position(position);
100+
randomAccessFile.position(position);
101101

102102
if (log.isDebugEnabled()) { log.debug("read header " + this); }
103103
}
@@ -149,7 +149,7 @@ public void setFormatId(int formatId) throws IOException {
149149
buf.putInt(formatId);
150150
buf.flip();
151151
while (buf.hasRemaining()) {
152-
fc.write(buf, FORMAT_ID_HEADER + buf.position());
152+
file.write(buf, FORMAT_ID_HEADER + buf.position());
153153
}
154154
this.formatId = formatId;
155155
}
@@ -165,7 +165,7 @@ public void setTimestamp(long timestamp) throws IOException {
165165
buf.putLong(timestamp);
166166
buf.flip();
167167
while (buf.hasRemaining()) {
168-
fc.write(buf, TIMESTAMP_HEADER + buf.position());
168+
file.write(buf, TIMESTAMP_HEADER + buf.position());
169169
}
170170
this.timestamp = timestamp;
171171
}
@@ -181,7 +181,7 @@ public void setState(byte state) throws IOException {
181181
buf.put(state);
182182
buf.flip();
183183
while (buf.hasRemaining()) {
184-
fc.write(buf, STATE_HEADER + buf.position());
184+
file.write(buf, STATE_HEADER + buf.position());
185185
}
186186
this.state = state;
187187
}
@@ -202,11 +202,11 @@ public void setPosition(long position) throws IOException {
202202
buf.putLong(position);
203203
buf.flip();
204204
while (buf.hasRemaining()) {
205-
fc.write(buf, CURRENT_POSITION_HEADER + buf.position());
205+
file.write(buf, CURRENT_POSITION_HEADER + buf.position());
206206
}
207207

208208
this.position = position;
209-
fc.position(position);
209+
file.position(position);
210210
}
211211

212212
/**
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package bitronix.tm.journal;
2+
3+
import java.io.UnsupportedEncodingException;
4+
import java.nio.ByteBuffer;
5+
6+
public final class ByteBufferUtil {
7+
8+
private ByteBufferUtil() {
9+
}
10+
11+
public static ByteBuffer createByteBuffer(final String input)
12+
throws UnsupportedEncodingException {
13+
final byte[] inputArray = input.getBytes("UTF-8");
14+
final ByteBuffer byteBuffer = ByteBuffer.wrap(inputArray);
15+
return byteBuffer;
16+
}
17+
18+
}

0 commit comments

Comments
 (0)