jamesfredley commented on code in PR #16631:
URL: https://github.com/apache/grails-core/pull/16631#discussion_r4373361018


##########
grails-datamapping-core/src/main/groovy/org/apache/grails/data/core/querytail/QueryTailCapture.groovy:
##########
@@ -0,0 +1,369 @@
+/*
+ *  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.querytail
+
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import java.nio.channels.FileLock
+import java.nio.channels.OverlappingFileLockException
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.StandardOpenOption
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicLong
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+/**
+ * Writes the statements an application runs to the capture file the {@code 
query-tail} command
+ * tails. Each session factory, Neo4j driver or MongoDB client that captures 
opens its own capture
+ * and closes it when it closes; the captures of one file share it, so every 
data source of an
+ * application writes to the one file. The file is truncated when the first 
capture opens it, so it
+ * holds the statements of the current run only, and its first line names that 
run. It is closed
+ * when the last capture closes, and opened afresh by the next one. Each 
statement is written whole
+ * and at once, so that the command sees it as soon as it is recorded.
+ *
+ * <p>JVMs that run at the same time with capture enabled, such as the 
parallel forks of a test task,
+ * share the file: a lock file beside it tells the first to start, which 
truncates the file and names
+ * the run, from the others, which append to that run without truncating it 
and number their
+ * requests apart, so that the command reads one run from all of them.</p>
+ *
+ * <p>A statement is recorded without its values: {@link QueryTailRedactor} 
replaces its literals and
+ * drops its comments before it is written, in the {@link #syntax} of the 
database it runs on. A
+ * statement that is not text, such as a MongoDB command, has its values taken 
out by whoever records
+ * it, through {@link #recordRedacted}.</p>
+ *
+ * <p>A capture is only opened when {@link QueryTailSettings#SETTING_ENABLED} 
is set; an application
+ * started without it never reaches this class.</p>
+ *
+ * @since 8.1
+ */
+@CompileStatic
+@Slf4j
+class QueryTailCapture implements Closeable {
+
+    private static final Map<String, CaptureFile> OPEN = new HashMap<>()
+
+    /** The file the statements are written to. */
+    final File file
+
+    /** The syntax of the database the statements of this capture run on, 
which their values are found by. */
+    volatile QueryTailSyntax syntax = QueryTailSyntax.UNKNOWN
+
+    private final CaptureFile captureFile
+    private final AtomicBoolean closed = new AtomicBoolean()
+
+    private QueryTailCapture(File file, CaptureFile captureFile) {
+        this.file = file
+        this.captureFile = captureFile
+    }
+
+    /**
+     * Opens a capture of a file. The first capture of a file truncates it; a 
capture opened while
+     * another of the same file is open shares it. Every capture opened must 
be closed.
+     *
+     * @param file the capture file
+     * @return the capture
+     * @throws UncheckedIOException when the file cannot be created
+     */
+    static QueryTailCapture open(File file) {
+        String key
+        try {
+            key = file.canonicalPath
+        }
+        catch (IOException e) {
+            throw new UncheckedIOException("Cannot resolve the query-tail 
capture file ${file}: ${e.message}", e)
+        }
+        synchronized (OPEN) {
+            CaptureFile captureFile = OPEN.get(key)
+            if (captureFile == null) {
+                captureFile = new CaptureFile(file, key)
+                OPEN.put(key, captureFile)
+                if (captureFile.joined) {
+                    log.info('Capturing statements to {} for query-tail, in 
the run of another JVM writing it', file)
+                }
+                else {
+                    log.info('Capturing statements to {} for query-tail', file)
+                }
+            }
+            captureFile.captures++
+            return new QueryTailCapture(file, captureFile)
+        }
+    }
+
+    /**
+     * Opens the capture the settings name.
+     *
+     * @param settings the query-tail settings
+     * @return the capture
+     */
+    static QueryTailCapture open(QueryTailSettings settings) {
+        open(settings.resolveFile())
+    }
+
+    /**
+     * Records a statement that is about to run on the current thread, without 
its values.
+     *
+     * @param dataSource the connection source name the statement runs against
+     * @param sql the statement
+     */
+    void record(String dataSource, String sql) {
+        write(dataSource, QueryTailRedactor.redact(sql, syntax))
+    }
+
+    /**
+     * Records a statement that is about to run on the current thread and that 
holds no value any
+     * more: a MongoDB command, which is a document rather than text, is 
redacted field by field by
+     * the listener that records it. Nothing of the statement is changed here.
+     *
+     * @param dataSource the connection source name the statement runs against
+     * @param statement the statement, with its values already taken out
+     */
+    void recordRedacted(String dataSource, String statement) {
+        write(dataSource, statement)
+    }
+
+    private void write(String dataSource, String statement) {
+        if (closed.get()) {
+            return
+        }
+        QueryTailRecord record = new QueryTailRecord(timestamp: 
System.currentTimeMillis(), dataSource: dataSource,
+                sql: statement)
+        captureFile.write(captureFile.attribution.attribute(record))
+    }
+
+    /**
+     * @return whether this capture is still writing to its file
+     */
+    boolean isOpen() {
+        !closed.get() && captureFile.open
+    }
+
+    /**
+     * Closes this capture. The file is closed once every capture of it is.
+     */
+    @Override
+    void close() {
+        if (!closed.compareAndSet(false, true)) {
+            return
+        }
+        boolean last
+        synchronized (OPEN) {
+            last = --captureFile.captures == 0
+            if (last && OPEN.get(captureFile.key).is(captureFile)) {

Review Comment:
   The capture is removed from `OPEN` before `close()` releases the file lock. 
Another thread can open the same file in that gap, see the old lock, and join 
without a writer lock of its own. When the old close finishes, another JVM can 
truncate the file that is still being written.
   
   Remove the entry and release the lock together, so a replacement cannot 
start until the old lock is gone.



-- 
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