Load chat history from the end of the log only
Chat logs grow forever, and every connect and every private chat opened read the whole log to keep its last 1000 entries. History now reads the last 4 MiB, which holds far more than that; searching still reads the whole log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import com.ts3client.text.TsLink;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -28,6 +30,11 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public final class ChatLogStore {
|
||||
public static final int HISTORY_LIMIT = 1000;
|
||||
/**
|
||||
* How much of the end of a log history is read from: far more than {@link #HISTORY_LIMIT}
|
||||
* entries take, without reading years of log on every connect.
|
||||
*/
|
||||
private static final int HISTORY_TAIL_BYTES = 4 << 20;
|
||||
|
||||
private static final DateTimeFormatter BEGIN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
@@ -57,15 +64,17 @@ public final class ChatLogStore {
|
||||
}
|
||||
|
||||
public List<String> serverHtmlHistory() throws IOException {
|
||||
return last(readHtml(log(ChatLogScope.SERVER)), HISTORY_LIMIT);
|
||||
return last(readHtml(log(ChatLogScope.SERVER), HISTORY_TAIL_BYTES), HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
public List<String> channelHtmlHistory() throws IOException {
|
||||
return last(readTextMessages(log(ChatLogScope.CHANNEL), ChatLogScope.CHANNEL), HISTORY_LIMIT);
|
||||
return last(readTextMessages(log(ChatLogScope.CHANNEL), ChatLogScope.CHANNEL, HISTORY_TAIL_BYTES),
|
||||
HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
public List<String> privateHtmlHistory(String clientUniqueId) throws IOException {
|
||||
return last(readTextMessages(privateLog(clientUniqueId), ChatLogScope.PRIVATE), HISTORY_LIMIT);
|
||||
return last(readTextMessages(privateLog(clientUniqueId), ChatLogScope.PRIVATE, HISTORY_TAIL_BYTES),
|
||||
HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
/** Entries of a whole log that match a query, in the order they were written. */
|
||||
@@ -84,8 +93,9 @@ public final class ChatLogStore {
|
||||
public SearchResult search(ChatLogScope scope, String clientUniqueId, TextQuery query, int limit)
|
||||
throws IOException {
|
||||
List<String> all = scope == ChatLogScope.SERVER
|
||||
? readHtml(log(scope))
|
||||
: readTextMessages(scope == ChatLogScope.PRIVATE ? privateLog(clientUniqueId) : log(scope), scope);
|
||||
? readHtml(log(scope), Long.MAX_VALUE)
|
||||
: readTextMessages(scope == ChatLogScope.PRIVATE ? privateLog(clientUniqueId) : log(scope), scope,
|
||||
Long.MAX_VALUE);
|
||||
List<String> hits = new ArrayList<>();
|
||||
for (String entry : all) {
|
||||
if (query.matches(HtmlText.plain(entry))) hits.add(entry);
|
||||
@@ -166,10 +176,11 @@ public final class ChatLogStore {
|
||||
if (!Files.exists(files.txt)) Files.createFile(files.txt);
|
||||
}
|
||||
|
||||
private static List<String> readHtml(LogFiles files) throws IOException {
|
||||
/** @param tailBytes how much of the end of the log to read */
|
||||
private static List<String> readHtml(LogFiles files, long tailBytes) throws IOException {
|
||||
if (!Files.isRegularFile(files.html)) return Collections.emptyList();
|
||||
List<String> entries = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(files.html, StandardCharsets.UTF_8)) {
|
||||
for (String line : readLines(files.html, tailBytes)) {
|
||||
if (line.startsWith("<p ")) entries.add(line);
|
||||
}
|
||||
return entries;
|
||||
@@ -179,12 +190,13 @@ public final class ChatLogStore {
|
||||
* A message is written as it was sent, so one with line breaks spans several lines of the
|
||||
* log: a line that starts no entry of its own continues the message before it.
|
||||
*/
|
||||
private static List<String> readTextMessages(LogFiles files, ChatLogScope scope) throws IOException {
|
||||
private static List<String> readTextMessages(LogFiles files, ChatLogScope scope, long tailBytes)
|
||||
throws IOException {
|
||||
if (!Files.isRegularFile(files.txt)) return Collections.emptyList();
|
||||
List<String> entries = new ArrayList<>();
|
||||
Matcher message = null;
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (String line : Files.readAllLines(files.txt, StandardCharsets.UTF_8)) {
|
||||
for (String line : readLines(files.txt, tailBytes)) {
|
||||
Matcher m = TEXT_MESSAGE.matcher(line);
|
||||
boolean begin = line.startsWith("*** ");
|
||||
if (!m.matches() && !begin) {
|
||||
@@ -205,6 +217,22 @@ public final class ChatLogStore {
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** The lines in the last {@code tailBytes} of a file, less the one that was cut into. */
|
||||
private static List<String> readLines(Path file, long tailBytes) throws IOException {
|
||||
try (SeekableByteChannel channel = Files.newByteChannel(file)) {
|
||||
long size = channel.size();
|
||||
if (size <= tailBytes) return Files.readAllLines(file, StandardCharsets.UTF_8);
|
||||
ByteBuffer tail = ByteBuffer.allocate((int) tailBytes);
|
||||
channel.position(size - tailBytes);
|
||||
int read = 0;
|
||||
while (tail.hasRemaining() && read >= 0) read = channel.read(tail);
|
||||
String text = new String(tail.array(), 0, tail.position(), StandardCharsets.UTF_8);
|
||||
List<String> lines = new ArrayList<>(List.of(text.split("\r?\n", -1)));
|
||||
lines.remove(0);
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> last(List<String> entries, int limit) {
|
||||
if (entries.size() <= limit) return entries;
|
||||
return new ArrayList<>(entries.subList(entries.size() - limit, entries.size()));
|
||||
|
||||
@@ -80,4 +80,20 @@ class ChatLogStoreTest {
|
||||
assertTrue(history.get(0).contains("first line<br><br>third line"), history.get(0));
|
||||
assertTrue(history.get(1).contains("reply"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void historyComesFromTheEndOfALongLog() throws Exception {
|
||||
ChatLogStore logs = new ChatLogStore(temp, "+Tyg2JtxE8vRNZp+JiUBnmBh0MY=");
|
||||
logs.appendMessage(ChatLogScope.CHANNEL, false, 5, "uid", "alice", "start");
|
||||
Path txt = logs.serverDir().resolve("channel.txt");
|
||||
StringBuilder filler = new StringBuilder();
|
||||
for (int i = 0; i < 200_000; i++) filler.append("<12:00:00> alice: žluťoučký kůň ").append(i).append('\n');
|
||||
Files.writeString(txt, filler, java.nio.file.StandardOpenOption.APPEND);
|
||||
logs.appendMessage(ChatLogScope.CHANNEL, false, 5, "uid", "bob", "the latest");
|
||||
|
||||
List<String> history = logs.channelHtmlHistory();
|
||||
assertEquals(ChatLogStore.HISTORY_LIMIT, history.size());
|
||||
assertTrue(history.get(history.size() - 1).contains("the latest"));
|
||||
assertTrue(history.get(0).contains("žluťoučký kůň"), history.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user