All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arcadedb.server.ha.ReplicationLogFile Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2021-present Arcade Data Ltd ([email protected])
 *
 * Licensed 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.
 *
 * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
 * SPDX-License-Identifier: Apache-2.0
 */
package com.arcadedb.server.ha;

import com.arcadedb.database.Binary;
import com.arcadedb.engine.WALFile;
import com.arcadedb.log.LogManager;
import com.arcadedb.utility.FileUtils;
import com.arcadedb.utility.LockContext;
import com.arcadedb.utility.Pair;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.*;

/**
 * Replication Log File. Writes the messages to send to a remote node on reconnection.
 * Since April 4, 2020, multiple chunk files are managed. The message position is still a long, by simulating the position of a continuous large file rather
 * than small chunks. This reduces the impacts on the rest of the components. The chunk size is 64MB, so no message can be bigger than that.
 * 

* TODO: CONSIDER STRIPING MSG IF FROM THE HEADER BECAUSE THE MESSAGE NUMBER IS CONTAINED IN BOTH THE HEADER (+0 POSITION) AND THE PAYLOAD (+1 POSITION) * ( MSG ID + COMMAND( CMD ID + MSG ID + SERIALIZATION ) ) */ public class ReplicationLogFile extends LockContext { private final String filePath; private FileChannel lastChunkChannel; private FileChannel searchChannel = null; private long searchChannelChunkId = -1; private static final int BUFFER_HEADER_SIZE = Binary.LONG_SERIALIZED_SIZE + Binary.INT_SERIALIZED_SIZE; private final ByteBuffer bufferHeader = ByteBuffer.allocate(BUFFER_HEADER_SIZE); private static final int BUFFER_FOOTER_SIZE = Binary.INT_SERIALIZED_SIZE + Binary.LONG_SERIALIZED_SIZE; private final ByteBuffer bufferFooter = ByteBuffer.allocate(BUFFER_FOOTER_SIZE); private static final long MAGIC_NUMBER = 93719829258702L; private long lastMessageNumber = -1L; private final static long CHUNK_SIZE = 64L * 1024L * 1024L; private long chunkNumber = 0L; private WALFile.FLUSH_TYPE flushPolicy = WALFile.FLUSH_TYPE.NO; private ReplicationLogArchiveCallback archiveChunkCallback = null; private long totalArchivedChunks = 0L; private long maxArchivedChunks = 200L; private static final Comparator LOG_COMPARATOR = (file1, file2) -> { int seq1 = Integer.parseInt(file1.getName().substring(file1.getName().lastIndexOf(".") + 1)); int seq2 = Integer.parseInt(file2.getName().substring(file2.getName().lastIndexOf(".") + 1)); return seq1 - seq2; }; public interface ReplicationLogArchiveCallback { void archiveChunk(File chunkFile, int chunkId); } public static class Entry { public final long messageNumber; public final Binary payload; public final int length; public Entry(final long messageNumber, final Binary payload, final int length) { this.messageNumber = messageNumber; this.payload = payload; this.length = length; } } public ReplicationLogFile(final String filePath) throws FileNotFoundException { this.filePath = filePath; openLastFile(); } public void close() { executeInLock(() -> { lastChunkChannel.force(true); lastChunkChannel.close(); lastChunkChannel = null; if (searchChannel != null) { searchChannel.close(); searchChannel = null; } return null; }); } // NEVER USED public void drop() { close(); new File(filePath).delete(); } public long getLastMessageNumber() { return lastMessageNumber; } public boolean appendMessage(final ReplicationMessage message) { return (boolean) executeInLock(() -> { try { if (!checkMessageOrder(message)) return false; if (lastChunkChannel == null) return false; // UPDATE LAST MESSAGE NUMBER lastMessageNumber = message.messageNumber; final byte[] content = message.payload.toByteArray(); final int entrySize = BUFFER_HEADER_SIZE + content.length + BUFFER_FOOTER_SIZE; if (entrySize > CHUNK_SIZE) throw new IllegalArgumentException( "Cannot store in replication file messages bigger than " + FileUtils.getSizeAsString(CHUNK_SIZE)); if (lastChunkChannel.size() + entrySize > CHUNK_SIZE) archiveChunk(); // WRITE HEADER bufferHeader.clear(); bufferHeader.putLong(message.messageNumber); bufferHeader.putInt(content.length); bufferHeader.rewind(); lastChunkChannel.write(bufferHeader, lastChunkChannel.size()); // WRITE PAYLOAD lastChunkChannel.write(ByteBuffer.wrap(content), lastChunkChannel.size()); // WRITE FOOTER bufferFooter.clear(); bufferFooter.putInt(entrySize); bufferFooter.putLong(MAGIC_NUMBER); bufferFooter.rewind(); lastChunkChannel.write(bufferFooter, lastChunkChannel.size()); switch (flushPolicy) { case YES_FULL: lastChunkChannel.force(true); break; case YES_NOMETADATA: lastChunkChannel.force(false); break; } return true; } catch (final Exception e) { throw new ReplicationLogException("Error on writing message " + message.messageNumber + " to the replication log", e); } }); } public long findMessagePosition(final long messageNumberToFind) { // TODO: CHECK THE LAST MESSAGE AND DECIDE WHERE TO START EITHER FROM THE HEAD OR FROM THE TAIL return (long) executeInLock(() -> { // LOOK FOR THE RIGHT FILE long chunkId = chunkNumber; while (chunkId > -1) { if (!openChunk(chunkId)) return -1L; bufferHeader.clear(); searchChannel.read(bufferHeader, 0); bufferHeader.rewind(); final long chunkBeginMessageNumber = bufferHeader.getLong(); if (messageNumberToFind == chunkBeginMessageNumber) // MESSAGE FOUND AS FIRST MESSAGE OF THE CHUNK return chunkId * CHUNK_SIZE; else if (messageNumberToFind > chunkBeginMessageNumber) // CHUNK FOUND break; --chunkId; } final long fileSize = searchChannel.size(); for (long pos = 0; pos < fileSize; ) { bufferHeader.clear(); searchChannel.read(bufferHeader, pos); bufferHeader.rewind(); final long messageNumber = bufferHeader.getLong(); if (messageNumber == messageNumberToFind) // FOUND return pos + (chunkId * CHUNK_SIZE); if (messageNumber > messageNumberToFind) // NOT IN LOG ANYMORE return -1L; final int contentLength = bufferHeader.getInt(); pos += BUFFER_HEADER_SIZE + contentLength + BUFFER_FOOTER_SIZE; } return -1L; }); } public void setLastMessageNumber(final long lastMessageNumber) { this.lastMessageNumber = lastMessageNumber; } public Pair getMessage(final long positionInFile) { return (Pair) executeInLock(() -> { if (positionInFile < 0) throw new ReplicationLogException("Invalid position (" + positionInFile + ") in replication log file of size " + getSize()); if (positionInFile > (searchChannel.size() - BUFFER_HEADER_SIZE - BUFFER_FOOTER_SIZE) + (chunkNumber * CHUNK_SIZE)) throw new ReplicationLogException("Invalid position (" + positionInFile + ") in replication log file of size " + getSize()); final int chunkId = (int) (positionInFile / CHUNK_SIZE); if (!openChunk(chunkId)) throw new ReplicationLogException("Cannot find replication log file with chunk id " + chunkId); final long posInChunk = positionInFile % CHUNK_SIZE; // READ THE HEADER bufferHeader.clear(); searchChannel.read(bufferHeader, posInChunk); bufferHeader.rewind(); final long messageNumber = bufferHeader.getLong(); final int contentLength = bufferHeader.getInt(); // LogManager.instance() // .log(this, Level.FINE, "Read log message chunk=%d pos=%d msgNumber=%d length=%d chunkSize=%d", null, chunkId, posInChunk, messageNumber, // contentLength, searchChannel.size()); // READ THE PAYLOAD final ByteBuffer bufferPayload = ByteBuffer.allocate(contentLength); searchChannel.read(bufferPayload, posInChunk + BUFFER_HEADER_SIZE); // READ THE FOOTER bufferFooter.clear(); searchChannel.read(bufferFooter, posInChunk + BUFFER_HEADER_SIZE + contentLength); bufferFooter.rewind(); bufferFooter.getInt(); // ENTRY-SIZE final long magicNumber = bufferFooter.getLong(); if (magicNumber != MAGIC_NUMBER) throw new ReplicationLogException("Corrupted replication log file at position " + positionInFile); final long nextPos; if (posInChunk + BUFFER_HEADER_SIZE + contentLength + BUFFER_FOOTER_SIZE >= searchChannel.size()) // END OF CHUNK, SET NEXT POSITION AT THE BEGINNING OF THE NEXT CHUNK nextPos = (chunkId + 1L) * CHUNK_SIZE; else nextPos = positionInFile + BUFFER_HEADER_SIZE + contentLength + BUFFER_FOOTER_SIZE; return new Pair<>(new ReplicationMessage(messageNumber, new Binary(bufferPayload.rewind())), nextPos); }); } public boolean checkMessageOrder(final ReplicationMessage message) { if (lastMessageNumber > -1) { if (message.messageNumber < lastMessageNumber) { LogManager.instance().log(this, Level.WARNING, "Wrong sequence in message numbers. Last was %d and now receiving %d. Skip saving this entry (threadId=%d)", lastMessageNumber, message.messageNumber, Thread.currentThread().getId()); return false; } if (message.messageNumber != lastMessageNumber + 1) { LogManager.instance().log(this, Level.WARNING, "Found a jump (%d) in message numbers. Last was %d and now receiving %d. Skip saving this entry (threadId=%d)", (message.messageNumber - lastMessageNumber), lastMessageNumber, message.messageNumber, Thread.currentThread().getId()); return false; } } return true; } public ReplicationMessage getLastMessage() { return (ReplicationMessage) executeInLock(() -> { if (lastChunkChannel == null) return null; final long pos = lastChunkChannel.size(); if (pos == 0) // EMPTY FILE return null; if (pos < BUFFER_HEADER_SIZE + BUFFER_FOOTER_SIZE) { // TODO: SCAN FROM THE HEAD throw new ReplicationLogException( "Invalid position (" + pos + ") in replication log file of size " + lastChunkChannel.size()); } // READ THE FOOTER bufferFooter.clear(); lastChunkChannel.read(bufferFooter, pos - BUFFER_FOOTER_SIZE); bufferFooter.rewind(); final int entrySize = bufferFooter.getInt(); final long magicNumber = bufferFooter.getLong(); if (magicNumber != MAGIC_NUMBER) throw new ReplicationLogException("Corrupted replication log file"); // READ THE HEADER bufferHeader.clear(); lastChunkChannel.read(bufferHeader, pos - entrySize); bufferHeader.rewind(); final long messageNumber = bufferHeader.getLong(); final int contentLength = bufferHeader.getInt(); // READ THE PAYLOAD final ByteBuffer bufferPayload = ByteBuffer.allocate(contentLength); lastChunkChannel.read(bufferPayload, pos - entrySize + BUFFER_HEADER_SIZE); return new ReplicationMessage(messageNumber, new Binary(bufferPayload.rewind())); }); } public long getSize() { return (Long) executeInLock(new Callable<>() { @Override public Object call() { try { return lastChunkChannel.size() + (chunkNumber * CHUNK_SIZE); } catch (final IOException e) { // TRY REOPENING THE FILE LogManager.instance().log(this, Level.SEVERE, "Error on computing file size for last chunk (%d) in replication log '%s', reopening file...", e, chunkNumber, filePath); try { lastChunkChannel.close(); } catch (IOException ex) { // IGNORE IT } try { openLastFile(); return lastChunkChannel.size() + (chunkNumber * CHUNK_SIZE); } catch (IOException ex) { LogManager.instance() .log(this, Level.SEVERE, "Error on computing file size for last chunk (%d) in replication log '%s'", e, chunkNumber, filePath); throw new ReplicationLogException("Error on computing file size for last chunk in replication log", e); } } } }); } public WALFile.FLUSH_TYPE getFlushPolicy() { return flushPolicy; } public void setFlushPolicy(final WALFile.FLUSH_TYPE flushPolicy) { this.flushPolicy = flushPolicy; } public void setArchiveChunkCallback(final ReplicationLogArchiveCallback archiveChunkCallback) { this.archiveChunkCallback = archiveChunkCallback; } /** * Returns the maximum chunk files to keep. * * @return 0 for unlimited */ public int getMaxArchivedChunks() { return (int) maxArchivedChunks; } /** * Sets the maximum number of chunk files to keep. Circular rewriting will be used. * * @param maxArchivedChunks use 0 for unlimited */ public void setMaxArchivedChunks(final int maxArchivedChunks) { this.maxArchivedChunks = maxArchivedChunks; } @Override public String toString() { return filePath; } @Override protected RuntimeException manageExceptionInLock(final Throwable e) { if (e instanceof ReplicationLogException) throw (ReplicationLogException) e; return new ReplicationLogException("Error in replication log", e); } private void openLastFileChunk(final File logFile) throws FileNotFoundException { final String prefix = logFile.getName() + "."; final List fileChunks = Arrays.asList(logFile.getParentFile().listFiles(f -> f.getName().startsWith(prefix))); fileChunks.sort(LOG_COMPARATOR); totalArchivedChunks = fileChunks.isEmpty() ? 0 : fileChunks.size() - 1; final File lastFile = fileChunks.isEmpty() ? new File(logFile.getAbsolutePath() + ".0") : fileChunks.get(fileChunks.size() - 1); this.lastChunkChannel = new RandomAccessFile(lastFile, "rw").getChannel(); chunkNumber = Long.parseLong(lastFile.getName().substring(lastFile.getName().lastIndexOf(".") + 1)); } private void archiveChunk() throws IOException { // CREATE A NEW CHUNK FILE lastChunkChannel.force(true); lastChunkChannel.close(); lastChunkChannel = null; if (archiveChunkCallback != null) { final File archivedFile = new File(filePath + "." + chunkNumber); try { archiveChunkCallback.archiveChunk(archivedFile, (int) chunkNumber); } catch (final Exception e) { LogManager.instance() .log(this, Level.WARNING, "Error in replication log archive callback invoked on file '%s'", e, archivedFile); } } if (maxArchivedChunks > 0 && ++totalArchivedChunks > maxArchivedChunks) { // REMOVE THE OLDEST final File file2Remove = new File(filePath + "." + (chunkNumber - maxArchivedChunks)); if (file2Remove.exists()) file2Remove.delete(); --totalArchivedChunks; } final File f = new File(filePath + "." + (chunkNumber + 1)); lastChunkChannel = new RandomAccessFile(f, "rw").getChannel(); ++chunkNumber; } private boolean openChunk(final long chunkId) throws IOException { if (chunkId != searchChannelChunkId) { if (searchChannel != null) searchChannel.close(); final File chunkFile = new File(filePath + "." + chunkId); if (!chunkFile.exists()) { // CHUNK NOT FOUND (= NOT AVAILABLE, PROBABLY DELETED BECAUSE TOO OLD) searchChannel = null; searchChannelChunkId = -1L; LogManager.instance().log(this, Level.WARNING, "Replication log chunk file %d was not found", null, chunkId); return false; } searchChannel = new RandomAccessFile(chunkFile, "rw").getChannel(); searchChannelChunkId = chunkId; } return true; } private void openLastFile() throws FileNotFoundException { final File f = new File(filePath); if (!f.exists()) f.getParentFile().mkdirs(); openLastFileChunk(f); final ReplicationMessage lastMessage = getLastMessage(); if (lastMessage != null) lastMessageNumber = lastMessage.messageNumber; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy