/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.jdbc.store.file;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.core.io.IOCallback;
import org.apache.activemq.artemis.core.io.SequentialFile;
import org.apache.activemq.artemis.core.io.buffer.TimedBuffer;
import org.apache.activemq.artemis.core.journal.EncodingSupport;
import org.apache.activemq.artemis.core.journal.impl.SimpleWaitIOCallback;
import org.jboss.logging.Logger;
public class JDBCSequentialFile implements SequentialFile {
private static final Logger logger = Logger.getLogger(JDBCSequentialFile.class);
private final String filename;
private final String extension;
private boolean isOpen = false;
private boolean isCreated = false;
private long id = -1;
private long readPosition = 0;
private long writePosition = 0;
private final Executor executor;
private final JDBCSequentialFileFactory fileFactory;
private final Object writeLock;
private final JDBCSequentialFileFactoryDriver dbDriver;
// Allows DB Drivers to cache meta data.
private final Map<Object, Object> metaData = new ConcurrentHashMap<>();
JDBCSequentialFile(final JDBCSequentialFileFactory fileFactory,
final String filename,
final Executor executor,
final JDBCSequentialFileFactoryDriver driver,
final Object writeLock) throws SQLException {
this.fileFactory = fileFactory;
this.filename = filename;
this.extension = filename.contains(".") ? filename.substring(filename.lastIndexOf(".") + 1, filename.length()) : "";
this.executor = executor;
this.writeLock = writeLock;
this.dbDriver = driver;
}
void setWritePosition(int writePosition) {
this.writePosition = writePosition;
}
@Override
public boolean isOpen() {
return isOpen;
}
@Override
public boolean exists() {
if (isCreated) return true;
try {
return fileFactory.listFiles(extension).contains(filename);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
fileFactory.onIOError(e, "Error checking JDBC file exists.", this);
return false;
}
}
@Override
public void open() throws Exception {
try {
if (!isOpen) {
synchronized (writeLock) {
dbDriver.openFile(this);
isCreated = true;
isOpen = true;
}
}
} catch (SQLException e) {
fileFactory.onIOError(e, "Error attempting to open JDBC file.", this);
}
}
@Override
public void open(int maxIO, boolean useExecutor) throws Exception {
open();
}
@Override
public boolean fits(int size) {
return writePosition + size <= dbDriver.getMaxSize();
}
@Override
public int calculateBlockStart(int position) throws Exception {
return 0;
}
@Override
public String getFileName() {
return filename;
}
@Override
public void fill(int size) throws Exception {
// Do nothing
}
@Override
public void delete() throws IOException, InterruptedException, ActiveMQException {
try {
if (isCreated) {
synchronized (writeLock) {
dbDriver.deleteFile(this);
}
}
} catch (SQLException e) {
fileFactory.onIOError(e, "Error deleting JDBC file.", this);
}
}
private synchronized int internalWrite(byte[] data, IOCallback callback) {
try {
synchronized (writeLock) {
int noBytes = dbDriver.writeToFile(this, data);
seek(noBytes);
if (logger.isTraceEnabled()) {
logger.trace("Write: ID: " + this.getId() + " FileName: " + this.getFileName() + size());
}
if (callback != null)
callback.done();
return noBytes;
}
} catch (Exception e) {
if (callback != null)
callback.onError(ActiveMQExceptionType.IO_ERROR.getCode(), e.getMessage());
fileFactory.onIOError(e, "Error writing to JDBC file.", this);
}
return 0;
}
public synchronized int internalWrite(ActiveMQBuffer buffer, IOCallback callback) {
byte[] data = new byte[buffer.readableBytes()];
buffer.readBytes(data);
return internalWrite(data, callback);
}
private synchronized int internalWrite(ByteBuffer buffer, IOCallback callback) {
return internalWrite(buffer.array(), callback);
}
private void scheduleWrite(final ActiveMQBuffer bytes, final IOCallback callback) {
executor.execute(() -> {
internalWrite(bytes, callback);
});
}
private void scheduleWrite(final ByteBuffer bytes, final IOCallback callback) {
executor.execute(() -> {
internalWrite(bytes, callback);
});
}
synchronized void seek(long noBytes) {
writePosition += noBytes;
}
@Override
public void write(ActiveMQBuffer bytes, boolean sync, IOCallback callback) throws Exception {
// We ignore sync since we schedule writes straight away.
scheduleWrite(bytes, callback);
}
@Override
public void write(ActiveMQBuffer bytes, boolean sync) throws Exception {
write(bytes, sync, null);
}
@Override
public void write(EncodingSupport bytes, boolean sync, IOCallback callback) throws Exception {
ActiveMQBuffer data = ActiveMQBuffers.fixedBuffer(bytes.getEncodeSize());
bytes.encode(data);
scheduleWrite(data, callback);
}
@Override
public void write(EncodingSupport bytes, boolean sync) throws Exception {
write(bytes, sync, null);
}
@Override
public void writeDirect(ByteBuffer bytes, boolean sync, IOCallback callback) {
if (callback == null) {
SimpleWaitIOCallback waitIOCallback = new SimpleWaitIOCallback();
try {
scheduleWrite(bytes, waitIOCallback);
waitIOCallback.waitCompletion();
} catch (Exception e) {
waitIOCallback.onError(ActiveMQExceptionType.IO_ERROR.getCode(), "Error writing to JDBC file.");
fileFactory.onIOError(e, "Failed to write to file.", this);
}
} else {
scheduleWrite(bytes, callback);
}
}
@Override
public void writeDirect(ByteBuffer bytes, boolean sync) throws Exception {
writeDirect(bytes, sync, null);
// Are we meant to block here?
}
@Override
public synchronized int read(ByteBuffer bytes, final IOCallback callback) throws SQLException {
synchronized (writeLock) {
try {
int read = dbDriver.readFromFile(this, bytes);
readPosition += read;
if (callback != null)
callback.done();
return read;
} catch (SQLException e) {
if (callback != null)
callback.onError(ActiveMQExceptionType.IO_ERROR.getCode(), e.getMessage());
fileFactory.onIOError(e, "Error reading from JDBC file.", this);
}
return 0;
}
}
@Override
public int read(ByteBuffer bytes) throws Exception {
return read(bytes, null);
}
@Override
public void position(long pos) throws IOException {
readPosition = pos;
}
@Override
public long position() {
return readPosition;
}
@Override
public void close() throws Exception {
isOpen = false;
sync();
fileFactory.sequentialFileClosed(this);
}
@Override
public void sync() throws IOException {
final SimpleWaitIOCallback callback = new SimpleWaitIOCallback();
executor.execute(callback::done);
try {
callback.waitCompletion();
} catch (Exception e) {
callback.onError(ActiveMQExceptionType.IO_ERROR.getCode(), "Error during JDBC file sync.");
fileFactory.onIOError(e, "Error during JDBC file sync.", this);
}
}
@Override
public long size() throws Exception {
return writePosition;
}
@Override
public void renameTo(String newFileName) throws Exception {
synchronized (writeLock) {
try {
dbDriver.renameFile(this, newFileName);
} catch (SQLException e) {
fileFactory.onIOError(e, "Error renaming JDBC file.", this);
}
}
}
@Override
public SequentialFile cloneFile() {
try {
JDBCSequentialFile clone = new JDBCSequentialFile(fileFactory, filename, executor, dbDriver, writeLock);
return clone;
} catch (Exception e) {
fileFactory.onIOError(e, "Error cloning JDBC file.", this);
}
return null;
}
@Override
public void copyTo(SequentialFile cloneFile) throws Exception {
JDBCSequentialFile clone = (JDBCSequentialFile) cloneFile;
try {
synchronized (writeLock) {
clone.open();
dbDriver.copyFileData(this, clone);
}
} catch (Exception e) {
fileFactory.onIOError(e, "Error copying JDBC file.", this);
}
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getFilename() {
return filename;
}
public String getExtension() {
return extension;
}
// Only Used by Journal, no need to implement.
@Override
public void setTimedBuffer(TimedBuffer buffer) {
}
// Only Used by replication, no need to implement.
@Override
public File getJavaFile() {
return null;
}
public void addMetaData(Object key, Object value) {
metaData.put(key, value);
}
public Object getMetaData(Object key) {
return metaData.get(key);
}
}