jdaugherty commented on code in PR #16631: URL: https://github.com/apache/grails-core/pull/16631#discussion_r4371545960
########## grails-datamapping-core/src/cli/groovy/org/apache/grails/data/core/cli/querytail/QueryTailFileReader.groovy: ########## @@ -0,0 +1,259 @@ +/* + * 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 + * + * https://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.grails.data.core.cli.querytail + +import java.nio.charset.StandardCharsets + +import groovy.transform.CompileStatic + +import org.apache.grails.data.core.querytail.QueryTailRecord + +/** + * Follows a capture file the way {@code tail -f} does: each poll returns the records written + * since the last one, up to a {@link #CHUNK chunk} of the file at a time, so that a capture of any + * size is read in pieces; a reader that is {@link #isBehind() behind} has more to read without + * waiting. A line that has no terminator yet is left for the next poll, so a record the + * application is still writing is never read in half, and a line longer than a chunk is dropped + * up to its end, as a line that is not a record is. An application that starts again + * truncates the file and names its new run in the first line; when that line names another run, + * or the file shrinks, reading starts over from the beginning. The run tells a restart apart from + * a file that grew past where the reader was before it was next polled. A file truncated between + * being measured and being read, so that the read comes up short, is a restart too: it is read again + * from the beginning in the same poll. A poll that finds the file as long as it left it reads nothing, + * not even its first line, so an idle command costs nothing; a restart that has written nothing yet + * is noticed with its first statement. + * + * @since 8.1 + */ +@CompileStatic +class QueryTailFileReader implements Closeable { + + private static final byte NEWLINE = (byte) 10 + + /** How much of the file the first line is looked for in; a header is far shorter. */ + private static final int HEADER_LIMIT = 256 + + /** How much of the file one poll reads at most, in bytes. */ + public static final int CHUNK = 1024 * 1024 + + final File file + final int chunk + private long position + private long measured = -1L + private String run + private boolean restarted + private boolean behind + private boolean skipping + + /** + * @param file the capture file + * @param fromStart whether to read what the file already holds, or only what is written from now on + */ + QueryTailFileReader(File file, boolean fromStart) { + this(file, fromStart, CHUNK) + } + + /** + * @param file the capture file + * @param fromStart whether to read what the file already holds, or only what is written from now on + * @param chunk how much of the file one poll reads at most, in bytes + */ + QueryTailFileReader(File file, boolean fromStart, int chunk) { + this.file = file + this.chunk = chunk + this.position = fromStart ? 0 : file.length() + this.run = file.exists() ? readRun() : null + // opened in the middle of a line being written: the rest of that line is not a record + this.skipping = position > 0 && !endsLine(readFrom(position - 1, position)) + } + + /** + * @return the records written since the last poll, or since the reader was opened, up to a chunk of the file + */ + List<QueryTailRecord> poll() { + restarted = false + behind = false + // a read that comes up short is retried once, from the start; the next poll takes it from there + for (int attempt = 0; attempt < 2; attempt++) { + long length = file.length() + if (length == position && length == measured) { Review Comment: Fixed in 6945601254. Every poll now reads the first line, which names the run (a random UUID per start), before it treats an unchanged length as idle. A poll is idle only when the run and the length are both unchanged. A restart that rewrites the capture to the same number of bytes is now reported as a restart, and its records are read from the start. That holds for a DevTools restart in the same JVM too, so the file's modified time would not have been a reliable marker. An idle poll now costs one read of at most 256 bytes every 250 ms. The old test that an idle poll never opens the file is replaced. Tests: - `QueryTailFileReaderSpec`: a new run exactly as long as what was read is reported as restarted, with run `run-2` and its own records; the next poll is idle; appended records are read without a restart. - `QueryTailRunnerSpec`: the restart stream feature now also covers a new run as long as the last one, and checks that the new run's records are shown as a new request. Against the previous reader, both same-length cases fail and the longer-run case passes. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
