/*
* Galaxy
* Copyright (c) 2012-2014, Parallel Universe Software Co. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 3.0
* as published by the Free Software Foundation.
*/
package co.paralleluniverse.galaxy.jdbc;
import static co.paralleluniverse.common.logging.LoggingUtils.hex;
import co.paralleluniverse.common.spring.Component;
import co.paralleluniverse.galaxy.server.MainMemoryDB;
import co.paralleluniverse.galaxy.server.MainMemoryEntry;
import com.google.common.base.Throwables;
import java.beans.ConstructorProperties;
import java.io.PrintStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author pron
*/
public class SQLDB extends Component implements MainMemoryDB {
private static final Logger LOG = LoggerFactory.getLogger(SQLDB.class);
private final DataSource dataSource;
private String username;
private String password;
private String schema = "pugalaxy";
private String tableName = "memory";
private String table;
private String allocationTableName = "allocation";
private String allocTable;
private String bigintType;
private String smallintType;
private String varbinaryType;
private int maxItemSize = 1024;
private boolean useUpdateableCursors = false;
private Connection conn;
private PreparedStatement casOwner;
private PreparedStatement getOwner;
private PreparedStatement deleteOwner;
private PreparedStatement insertLine;
private PreparedStatement setLine;
private PreparedStatement getLine;
private PreparedStatement deleteLine;
private PreparedStatement selectAll;
private PreparedStatement getMaxId;
private PreparedStatement addAllocation;
private PreparedStatement getAllocation;
private static final Object TRANSACTION = new Object();
@ConstructorProperties({"name", "dataSource"})
public SQLDB(String name, DataSource dataSource) {
super(name);
this.dataSource = dataSource;
}
public void setPassword(String password) {
assertDuringInitialization();
this.password = password;
}
public void setUsername(String username) {
assertDuringInitialization();
this.username = username;
}
public void setSchema(String schema) {
assertDuringInitialization();
this.schema = schema;
}
public void setTableName(String tableName) {
assertDuringInitialization();
this.tableName = tableName;
}
public void setAllocationTableName(String tableName) {
assertDuringInitialization();
this.allocationTableName = tableName;
}
public void setMaxItemSize(int maxItemSize) {
assertDuringInitialization();
this.maxItemSize = maxItemSize;
}
public void setUseUpdateableCursors(boolean useUpdateableCursors) {
assertDuringInitialization();
this.useUpdateableCursors = useUpdateableCursors;
}
public void setBigintType(String bigintType) {
assertDuringInitialization();
this.bigintType = bigintType;
}
public void setVarbinaryType(String varbinaryType) {
assertDuringInitialization();
this.varbinaryType = varbinaryType;
}
public void setSmallintType(String smallintType) {
assertDuringInitialization();
this.smallintType = smallintType;
}
@Override
protected void init() throws Exception {
super.init();
LOG.info("Connecting to database {}", dataSource);
if (username != null)
conn = dataSource.getConnection(username, password);
else
conn = dataSource.getConnection();
LOG.info("Connection successful");
initDbTypes();
this.table = schema + "." + tableName;
this.allocTable = schema + "." + allocationTableName;
initTable();
initPreparedStatements();
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
}
private void initTable() throws SQLException {
try {
try (Statement stmt = conn.createStatement()) {
String createTable = "CREATE TABLE " + table + " "
+ "(id " + bigintType + " PRIMARY KEY, "
+ "owner " + smallintType + " NOT NULL, "
+ "version " + bigintType + " NOT NULL, "
+ "data " + createVarbinary(maxItemSize)
+ ")";
LOG.debug("Creating table: {}", createTable);
stmt.executeUpdate(createTable);
stmt.executeUpdate("CREATE INDEX owner_index ON " + table + "(owner)");
createTable = "CREATE TABLE " + allocTable + " "
+ "(id " + bigintType + " PRIMARY KEY, "
+ "end " + bigintType + " NOT NULL, "
+ "owner " + smallintType + " NOT NULL "
+ ")";
LOG.debug("Creating table: {}", createTable);
stmt.executeUpdate(createTable);
}
} catch (SQLException e) {
LOG.debug("SQLException caught: {} - {}", e.getClass().getName(), e.getMessage());
}
}
private void initPreparedStatements() throws SQLException {
getMaxId();
dump(null);
insert(0, (short) 0, 0, null, null);
read(0);
write(0, (short) 0, 0, null, null);
delete(0, null);
removeOwner((short) 0);
if (useUpdateableCursors)
casOwnerUpdateableCursor(0, (short) 0, (short) 0);
else {
casOwnerUpdate(0, (short) 0, (short) 0);
getOwner(0);
}
allocate((short) 0, 0, 0);
findAllocation(0);
}
private void initDbTypes() throws SQLException {
if (bigintType == null || smallintType == null || varbinaryType == null) {
final Map<Integer, String> types = new HashMap<Integer, String>();
final DatabaseMetaData dmd = conn.getMetaData();
try (ResultSet rs = dmd.getTypeInfo()) {
while (rs.next()) {
final int jdbcType = rs.getInt("DATA_TYPE");
final String typeName = rs.getString("TYPE_NAME");
types.put(jdbcType, typeName);
}
}
if (bigintType == null)
bigintType = types.get(Types.BIGINT);
if (smallintType == null)
smallintType = types.get(Types.SMALLINT);
if (varbinaryType == null)
varbinaryType = types.get(Types.VARBINARY);
LOG.debug("BIGINT type is: {}", bigintType);
LOG.debug("SMALLINT type is: {}", smallintType);
LOG.debug("VARBINARY type is: {}", varbinaryType);
}
}
private String createVarbinary(int size) {
if (varbinaryType.contains("()"))
return varbinaryType.replace("()", "(" + size + ")");
else
return varbinaryType + "(" + size + ")";
}
@Override
public void close() {
try {
conn.close();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public Object beginTransaction() {
return TRANSACTION;
}
@Override
public void commit(Object txn) {
try {
LOG.debug("COMMIT");
assert txn == TRANSACTION;
conn.commit();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public void abort(Object txn) {
try {
LOG.debug("ROLLBACK");
assert txn == TRANSACTION;
conn.rollback();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public void write(long id, short owner, long version, byte[] data, Object txn) {
if (setLine == null) {
setLine = prepareStatement("UPDATE " + table + " SET version = ?, data = ? WHERE id = ? AND owner = ?");
return;
}
if (LOG.isDebugEnabled())
LOG.debug("WRITE " + id + " ver: " + version + " data: (" + data.length + " bytes)");
if (data.length > maxItemSize) {
LOG.error("Data length is {}, which is bigger than maxItemSize ({})", data.length, maxItemSize);
throw new RuntimeException("Data too big.");
}
try {
setLine.setLong(3, id);
setLine.setShort(4, owner);
setLine.setLong(1, version);
setLine.setBytes(2, data);
if (setLine.executeUpdate() < 1) {
LOG.debug("Setting line {} failed. Inserting.", id);
insert(id, (short) owner, version, data, txn);
} else if (txn == null) // insert() commits
conn.commit();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public MainMemoryEntry read(long id) {
if (getLine == null) {
getLine = prepareStatement("SELECT version, data FROM " + table + " WHERE id = ?");
return null;
}
try {
getLine.setLong(1, id);
try (ResultSet rs = getLine.executeQuery()) {
rs.next();
final long version = rs.getLong(1);
final byte[] data = rs.getBytes(2);
conn.commit();
return new MainMemoryEntry(version, data);
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public short casOwner(long id, short oldNode, short newNode) {
if (LOG.isDebugEnabled())
LOG.debug("CAS owner of {}: {} -> {}", new Object[]{id, oldNode, newNode});
try {
if (oldNode < 0) {
try {
insert(id, newNode, -1, null, null);
LOG.debug("CAS owner succeeded (insert).");
return newNode;
} catch (SQLException e) {
}
LOG.debug("CAS owner failed (insert).");
return getOwner(id);
} else {
if (useUpdateableCursors)
return casOwnerUpdateableCursor(id, oldNode, newNode);
else
return casOwnerUpdate(id, oldNode, newNode);
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
private short casOwnerUpdateableCursor(long id, short oldNode, short newNode) throws SQLException {
if (casOwner == null) {
casOwner = prepareStatement("SELECT owner FROM " + table + " WHERE id = ? FOR UPDATE", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
return 0;
}
final short res;
casOwner.setLong(1, id);
try (ResultSet rs = casOwner.executeQuery()) {
if (rs.next()) {
final short currentOwner = rs.getShort(1);
if (currentOwner != oldNode) {
LOG.debug("CAS owner failed (UC).");
res = currentOwner;
} else {
rs.updateShort(1, newNode);
LOG.debug("CAS owner succeeded (UC).");
res = newNode;
}
conn.commit();
return res;
} else {
LOG.debug("CAS owner failed (UC).");
return -1;
}
}
}
private short casOwnerUpdate(long id, short oldNode, short newNode) throws SQLException {
if (casOwner == null) {
casOwner = prepareStatement("UPDATE " + table + " SET owner = ? WHERE id = ? AND owner = ?");
return 0;
}
final short res;
casOwner.setLong(2, id);
casOwner.setShort(3, oldNode);
casOwner.setShort(1, newNode);
int rows = casOwner.executeUpdate();
if (rows > 0) {
LOG.debug("CAS owner succeeded.");
res = newNode;
} else {
LOG.debug("CAS owner failed.");
res = getOwner(id);
}
conn.commit();
return res;
}
private short getOwner(long id) throws SQLException {
if (getOwner == null) {
getOwner = conn.prepareStatement("SELECT owner FROM " + table + " WHERE id = ?");
return 0;
}
getOwner.setLong(1, id);
try (ResultSet rs = getOwner.executeQuery()) {
final short res = rs.next() ? rs.getShort(1) : (short) -1;
conn.commit();
return res;
}
}
private void insert(long id, short owner, long version, byte[] data, Object txn) throws SQLException {
if (insertLine == null) {
insertLine = prepareStatement("INSERT INTO " + table + " (id, owner, version, data) VALUES (?, ?, ?, ?)");
return;
}
insertLine.setLong(1, id);
insertLine.setShort(2, owner);
insertLine.setLong(3, version);
insertLine.setBytes(4, data);
insertLine.executeUpdate();
if (txn == null)
conn.commit();
}
@Override
public void delete(long id, Object txn) {
if (deleteLine == null) {
deleteLine = prepareStatement("DELETE FROM " + table + " WHERE id = ?");
return;
}
try {
deleteLine.setLong(1, id);
deleteLine.executeUpdate();
if (txn == null)
conn.commit();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public void removeOwner(short node) {
if (deleteOwner == null) {
deleteOwner = prepareStatement("UPDATE " + table + " SET owner = 0 WHERE owner = ?");
return;
}
try {
deleteOwner.setShort(1, node);
deleteOwner.executeUpdate();
conn.commit();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public void allocate(short owner, long start, int num) {
if (owner <= 0) {
addAllocation = prepareStatement("INSERT INTO " + allocTable + " (id, end, owner) VALUES (?, ?, ?)");
return;
}
try {
addAllocation.setLong(1, start);
addAllocation.setLong(2, start + num);
addAllocation.setShort(3, owner);
addAllocation.executeUpdate();
conn.commit();
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public short findAllocation(long ref) {
if (ref <= 0) {
getAllocation = prepareStatement("SELECT owner FROM " + allocTable + " WHERE id <= ? AND end > ?");
return 0;
}
try {
getAllocation.setLong(1, ref);
getAllocation.setLong(2, ref);
try (ResultSet rs = getAllocation.executeQuery()) {
final short res = rs.next() ? rs.getShort(1) : -1;
conn.commit();
return res;
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public long getMaxId() {
if (getMaxId == null) {
getMaxId = prepareStatement("SELECT MAX(id) FROM " + allocTable);
return 0;
}
try (ResultSet rs = getMaxId.executeQuery()) {
final long res = rs.next() ? rs.getLong(1) : 0;
conn.commit();
return res;
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
@Override
public void dump(PrintStream ps) {
if (selectAll == null) {
selectAll = prepareStatement("SELECT * FROM " + table);
return;
}
try {
ps.println("MEMORY");
ps.println("===========");
try (ResultSet rs = selectAll.executeQuery()) {
while (rs.next()) {
final long id = rs.getLong("id");
final short owner = rs.getShort("owner");
final long version = rs.getLong("version");
final byte[] data = rs.getBytes("data");
ps.println("Id : " + hex(id) + " owner: " + owner + " version: " + version + " data: (" + data.length + " bytes).");
}
conn.commit();
}
} catch (SQLException e) {
throw Throwables.propagate(e);
}
}
private PreparedStatement prepareStatement(String sql) {
try {
assertDuringInitialization();
return conn.prepareStatement(sql);
} catch (SQLException e) {
LOG.error("Error while preparing statement: " + sql, e);
throw new Error(e);
}
}
private PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) {
try {
assertDuringInitialization();
return conn.prepareStatement(sql, resultSetType, resultSetConcurrency);
} catch (SQLException e) {
LOG.error("Error while preparing statement: " + sql, e);
throw new Error(e);
}
}
}