jamesfredley commented on code in PR #16631: URL: https://github.com/apache/grails-core/pull/16631#discussion_r4371526281
########## 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: If the application restarts between polls and the new capture is the same number of bytes, this returns before it reads the new run header. `poll()` then reports no records and no restart, so the summary stays on the old run. Check the header, or another change marker, before treating an unchanged length as idle. -- 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]
