public H2LogStore(String path) { this.logger = LogManager.getLogger(this.getClass()); this.startIndex = new AtomicLong(); this.nextIndex = new AtomicLong(); this.lastEntry = new LogEntry(0, null, LogValueType.Application); try { Class.forName("org.h2.Driver"); this.connection = DriverManager.getConnection("jdbc:h2:" + path, "sa", ""); this.connection.setAutoCommit(false); boolean isNew = true; ResultSet tables = this.connection.createStatement().executeQuery("SHOW TABLES"); while (tables.next()) { if (TABLE_NAME.equalsIgnoreCase(tables.getString(1))) { isNew = false; break; } } tables.close(); if (isNew) { this.connection.createStatement().execute(CREATE_SEQUENCE_SQL); this.connection.createStatement().execute(CREATE_TABLE_SQL); this.connection.commit(); this.startIndex.set(1); this.nextIndex.set(1); } else { ResultSet rs = this.connection.createStatement().executeQuery("SELECT MIN(id), MAX(id) FROM LogStore"); if (rs.next()) { this.startIndex.set(rs.getLong(1)); this.nextIndex.set(rs.getLong(2) + 1); } else { this.startIndex.set(1); this.nextIndex.set(1); } rs.close(); rs = this.connection .createStatement() .executeQuery("SELECT TOP 1 * FROM LogStore ORDER BY id DESC"); if (rs.next()) { this.lastEntry = new LogEntry(rs.getLong(2), rs.getBytes(4), LogValueType.fromByte(rs.getByte(3))); } rs.close(); } } catch (Throwable error) { this.logger.error("failed to load or create log store database", error); throw new RuntimeException("failed to load or create a log store", error); } }
@Override public void applyLogPack(long index, byte[] logPack) { if (index < this.startIndex.get()) { throw new IllegalArgumentException("logIndex out of range"); } try { ByteArrayInputStream memoryStream = new ByteArrayInputStream(logPack); GZIPInputStream gzipStream = new GZIPInputStream(memoryStream); byte[] sizeBuffer = new byte[Integer.BYTES]; PreparedStatement ps = this.connection.prepareStatement(TRIM_TABLE_SQL); ps.setLong(1, index - 1); ps.execute(); ps = this.connection.prepareStatement(UPDATE_SEQUENCE_SQL); ps.setLong(1, index); ps.execute(); while (this.read(gzipStream, sizeBuffer)) { int size = BinaryUtils.bytesToInt(sizeBuffer, 0); byte[] entryData = new byte[size - Integer.BYTES]; if (!this.read(gzipStream, entryData)) { throw new RuntimeException("bad log pack, no able to read the log entry data"); } ByteBuffer buffer = ByteBuffer.wrap(entryData); long term = buffer.getLong(); byte valueType = buffer.get(); byte[] value = new byte[size - Long.BYTES - 1 - Integer.BYTES]; buffer.get(value); ps = this.connection.prepareStatement(INSERT_ENTRY_SQL); ps.setLong(1, term); ps.setByte(2, valueType); ps.setBytes(3, value); ps.execute(); this.lastEntry = new LogEntry(term, value, LogValueType.fromByte(valueType)); } this.connection.commit(); gzipStream.close(); } catch (Throwable error) { this.logger.error("failed to apply log pack", error); throw new RuntimeException("log store error", error); } }
@Override public boolean compact(long lastLogIndex) { if (lastLogIndex < this.startIndex.get()) { throw new IllegalArgumentException("index out of range"); } try { PreparedStatement ps = this.connection.prepareStatement(COMPACT_TABLE_SQL); ps.setLong(1, lastLogIndex); ps.execute(); if (this.nextIndex.get() - 1 <= lastLogIndex) { ps = this.connection.prepareStatement(UPDATE_SEQUENCE_SQL); ps.setLong(1, lastLogIndex + 1); ps.execute(); } this.connection.commit(); this.startIndex.set(lastLogIndex + 1); if (this.nextIndex.get() - 1 <= lastLogIndex) { this.nextIndex.set(lastLogIndex + 1); } // reload last entry ResultSet rs = this.connection .createStatement() .executeQuery("SELECT TOP 1 * FROM LogStore ORDER BY id DESC"); if (rs.next()) { this.lastEntry = new LogEntry(rs.getLong(2), rs.getBytes(4), LogValueType.fromByte(rs.getByte(3))); } else { this.lastEntry = new LogEntry(0, null, LogValueType.Application); } rs.close(); return true; } catch (Throwable error) { this.logger.error("failed to compact the log store", error); throw new RuntimeException("log store error", error); } }
@Override public LogEntry getLogEntryAt(long index) { if (index < this.startIndex.get()) { throw new IllegalArgumentException("index out of range"); } try { PreparedStatement ps = this.connection.prepareStatement(SELECT_ENTRY_SQL); ps.setLong(1, index); ResultSet rs = ps.executeQuery(); while (rs.next()) { return new LogEntry(rs.getLong(2), rs.getBytes(4), LogValueType.fromByte(rs.getByte(3))); } rs.close(); return null; } catch (Throwable error) { this.logger.error("failed to retrieve an entry at a specific index", error); throw new RuntimeException("log store error", error); } }
@Override public LogEntry[] getLogEntries(long start, long end) { if (start > end || start < this.startIndex.get()) { throw new IllegalArgumentException("index out of range"); } try { PreparedStatement ps = this.connection.prepareStatement(SELECT_RANGE_SQL); ps.setLong(1, start); ps.setLong(2, end); ResultSet rs = ps.executeQuery(); List<LogEntry> entries = new ArrayList<LogEntry>(); while (rs.next()) { entries.add( new LogEntry(rs.getLong(2), rs.getBytes(4), LogValueType.fromByte(rs.getByte(3)))); } rs.close(); return entries.toArray(new LogEntry[0]); } catch (Throwable error) { this.logger.error("failed to retrieve a range of entries", error); throw new RuntimeException("log store error", error); } }