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]

Reply via email to