jdaugherty commented on code in PR #16631: URL: https://github.com/apache/grails-core/pull/16631#discussion_r4375671891
########## 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: Fixed in 6306f94ef7. When the last capture of a file closes, it now releases the file and its lock inside the same `synchronized (OPEN)` block that removes the entry. A capture opened on the same path waits until the old lock is gone, then starts or joins a run with a writer lock of its own. The new feature "reopening waits for the last owner to finish its write and release its coordination lock" in `QueryTailCaptureSpec` covers this: - It holds the old writer in the middle of a write to a FIFO. - It closes the last capture and opens the same path while that write is still pending. - It checks that the open blocks rather than joining the closing writer. - Once the write drains, it checks that the replacement holds the writer lock itself. -- 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]
