Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import org.apache.maven.impl.SettingsUtilsV4;
import org.apache.maven.jline.FastTerminal;
import org.apache.maven.jline.MessageUtils;
import org.apache.maven.logging.AsyncDrainWriter;
import org.apache.maven.logging.BuildEventListener;
import org.apache.maven.logging.LoggingOutputStream;
import org.apache.maven.logging.ProjectBuildLogAppender;
Expand Down Expand Up @@ -415,24 +416,30 @@ protected Consumer<String> determineWriter(C context) {
}

protected Consumer<String> doDetermineWriter(C context) {
Consumer<String> raw;
if (context.options().logFile().isPresent()) {
Path logFile = context.cwd.resolve(context.options().logFile().get());
try {
PrintWriter printWriter = new PrintWriter(Files.newBufferedWriter(logFile), true);
context.closeables.add(printWriter);
return printWriter::println;
raw = printWriter::println;
} catch (IOException e) {
throw new MavenException("Unable to redirect logging to " + logFile, e);
}
} else {
// Given the terminal creation has been offloaded to a different thread,
// do not pass directly the terminal writer
return msg -> {
raw = msg -> {
PrintWriter pw = context.terminal.writer();
pw.println(msg);
pw.flush();
};
}
// Wrap with lock-free async drain to eliminate PrintWriter synchronized contention
// when multiple PhasingExecutor threads log concurrently during parallel model building.
AsyncDrainWriter asyncWriter = new AsyncDrainWriter(raw);
context.closeables.add(asyncWriter);
return asyncWriter;
}

protected void activateLogging(C context) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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
*
* http://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.maven.logging;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

/**
* A lock-free buffering wrapper around a {@link Consumer Consumer&lt;String&gt;} that
* eliminates contention when multiple threads log concurrently.
* <p>
* Callers enqueue messages into a {@link ConcurrentLinkedQueue} (lock-free),
* then attempt a non-blocking drain via {@link ReentrantLock#tryLock()}.
* If another thread is already draining, the caller returns immediately —
* its message will be picked up by the ongoing or next drain cycle.
* This ensures that at most one thread writes to the underlying consumer
* at any time, without blocking producers.
* <p>
* The pattern eliminates the {@code synchronized} contention in
* {@link java.io.PrintWriter#println(String)} that occurs when multiple
* {@link org.apache.maven.impl.util.PhasingExecutor} threads log during
* parallel model building.
*
* @since 4.0.0
*/
public class AsyncDrainWriter implements Consumer<String>, AutoCloseable {

private final Consumer<String> delegate;
private final ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
private final ReentrantLock drainLock = new ReentrantLock();

public AsyncDrainWriter(Consumer<String> delegate) {
this.delegate = delegate;
}

@Override
public void accept(String msg) {
queue.add(msg);
tryDrain();
}

/**
* Attempts a non-blocking drain of all queued messages.
* Only one thread drains at a time; others return immediately.
* After releasing the lock, a re-check ensures no message is
* stranded by a race between enqueue and the last poll.
*/
private void tryDrain() {
if (drainLock.tryLock()) {
try {
drain();
} finally {
drainLock.unlock();
}
// Re-check: a message may have been enqueued after our last poll()
// but before unlock(). The enqueuer's tryLock() would have failed,
// so we need to pick it up here.
if (!queue.isEmpty() && drainLock.tryLock()) {
try {
drain();
} finally {
drainLock.unlock();
}
}
}
}

private void drain() {
String m;
while ((m = queue.poll()) != null) {
delegate.accept(m);
}
}

/**
* Flushes all remaining buffered messages to the delegate.
* Blocks until the drain is complete — call this before shutdown
* to ensure no messages are lost.
*/
@Override
public void close() {
drainLock.lock();
try {
drain();
} finally {
drainLock.unlock();
}
}
}
Loading