Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve GC reclaim tests and avoid using finalize()
  • Loading branch information
vy committed Oct 9, 2024
commit 2022db7d3910bcd07b006452efc12e99482ca14c
Original file line number Diff line number Diff line change
Expand Up @@ -16,96 +16,111 @@
*/
package org.apache.logging.log4j.core;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import static org.apache.logging.log4j.core.GcHelper.awaitGarbageCollection;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.test.CoreLoggerContexts;
import org.junit.jupiter.api.BeforeAll;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.apache.logging.log4j.core.test.appender.ListAppender;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junitpioneer.jupiter.SetSystemProperty;

@Tag("functional")
public class EventParameterMemoryLeakTest {
class EventParameterMemoryLeakTest {

@Test
@SetSystemProperty(key = "log4j2.enableDirectEncoders", value = "true")
@SetSystemProperty(key = "log4j2.enableThreadLocals", value = "true")
void parameters_should_be_garbage_collected(final TestInfo testInfo) throws Throwable {
awaitGarbageCollection(() -> {
final ListAppender[] appenderRef = {null};
final Logger[] loggerRef = {null};
try (final LoggerContext ignored = createLoggerContext(testInfo, appenderRef, loggerRef)) {

// Log messages
final ParameterObject parameter = new ParameterObject("paramValue");
loggerRef[0].info("Message with parameter {}", parameter);
loggerRef[0].info(parameter);
loggerRef[0].info("test", new ObjectThrowable(parameter));
loggerRef[0].info("test {}", "hello", new ObjectThrowable(parameter));

@BeforeAll
public static void beforeClass() {
System.setProperty("log4j2.enableThreadlocals", "true");
System.setProperty("log4j2.enableDirectEncoders", "true");
System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "EventParameterMemoryLeakTest.xml");
// Verify the logging
final List<String> messages = appenderRef[0].getMessages();
assertThat(messages).hasSize(4);
assertThat(messages.get(0)).isEqualTo("Message with parameter %s", parameter.value);
assertThat(messages.get(1)).isEqualTo(parameter.value);
assertThat(messages.get(2))
.startsWith(String.format("test%n%s: %s", ObjectThrowable.class.getName(), parameter.value));
assertThat(messages.get(3))
.startsWith(
String.format("test hello%n%s: %s", ObjectThrowable.class.getName(), parameter.value));

// Return the GC subject
return parameter;
}
});
}

@Test
@SuppressWarnings("UnusedAssignment") // parameter set to null to allow garbage collection
public void testParametersAreNotLeaked() throws Exception {
final File file = new File("target", "EventParameterMemoryLeakTest.log");
assertTrue(!file.exists() || file.delete(), "Deleted old file before test");

final Logger log = LogManager.getLogger("com.foo.Bar");
final CountDownLatch latch = new CountDownLatch(1);
Object parameter = new ParameterObject("paramValue", latch);
log.info("Message with parameter {}", parameter);
log.info(parameter);
log.info("test", new ObjectThrowable(parameter));
log.info("test {}", "hello", new ObjectThrowable(parameter));
parameter = null;
CoreLoggerContexts.stopLoggerContext(file);
final BufferedReader reader = new BufferedReader(new FileReader(file));
final String line1 = reader.readLine();
final String line2 = reader.readLine();
final String line3 = reader.readLine();
final String line4 = reader.readLine();
final String line5 = reader.readLine();
reader.close();
// file.delete();
assertThat(line1, containsString("Message with parameter paramValue"));
assertThat(line2, containsString("paramValue"));
assertThat(line3, containsString("paramValue"));
assertThat(line4, containsString("paramValue"));
assertNull(line5, "Expected only six lines");
final GarbageCollectionHelper gcHelper = new GarbageCollectionHelper();
gcHelper.run();
try {
assertTrue(latch.await(30, TimeUnit.SECONDS), "Parameter should have been garbage collected");
} finally {
gcHelper.close();
}
private static LoggerContext createLoggerContext(
final TestInfo testInfo, final ListAppender[] appenderRef, final Logger[] loggerRef) {
final String loggerContextName = String.format("%s-LC", testInfo.getDisplayName());
final LoggerContext loggerContext = new LoggerContext(loggerContextName);
final String appenderName = "LIST";
final Configuration configuration = createConfiguration(appenderName);
loggerContext.start(configuration);
appenderRef[0] = configuration.getAppender(appenderName);
assertThat(appenderRef[0]).isNotNull();
final Class<?> testClass = testInfo.getTestClass().orElse(null);
assertThat(testClass).isNotNull();
loggerRef[0] = loggerContext.getLogger(testClass);
return loggerContext;
}

@SuppressWarnings("SameParameterValue")
private static Configuration createConfiguration(final String appenderName) {
final ConfigurationBuilder<BuiltConfiguration> configBuilder =
ConfigurationBuilderFactory.newConfigurationBuilder();
final LayoutComponentBuilder layoutComponentBuilder =
configBuilder.newLayout("PatternLayout").addAttribute("pattern", "%m");
final AppenderComponentBuilder appenderComponentBuilder =
configBuilder.newAppender(appenderName, "List").add(layoutComponentBuilder);
final RootLoggerComponentBuilder loggerComponentBuilder =
configBuilder.newRootLogger(Level.ALL).add(configBuilder.newAppenderRef(appenderName));
return configBuilder
.add(appenderComponentBuilder)
.add(loggerComponentBuilder)
.build(false);
}

private static final class ParameterObject {

private final String value;
private final CountDownLatch latch;

ParameterObject(final String value, final CountDownLatch latch) {
private ParameterObject(final String value) {
this.value = value;
this.latch = latch;
}

@Override
public String toString() {
return value;
}

@Override
protected void finalize() throws Throwable {
latch.countDown();
super.finalize();
}
}

private static final class ObjectThrowable extends RuntimeException {

private final Object object;

ObjectThrowable(final Object object) {
private ObjectThrowable(final Object object) {
super(String.valueOf(object));
this.object = object;
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.logging.log4j.core;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import org.junit.jupiter.api.function.ThrowingSupplier;

public final class GcHelper {

private GcHelper() {}

/**
* Waits for the value to be garbage collected.
*
* @param valueSupplier a value provider
*/
@SuppressWarnings({"unused", "UnusedAssignment"})
public static void awaitGarbageCollection(final ThrowingSupplier<?> valueSupplier) throws InterruptedException {

// Create the reference queue
final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
final Reference<?> ref;
try {
final Object value = valueSupplier.get();
ref = new PhantomReference<>(value, refQueue);
} catch (final Throwable error) {
throw new RuntimeException("failed obtaining value", error);
}

// Wait for the garbage collection
try (final GcPressureGenerator ignored = GcPressureGenerator.ofStarted()) {
final Reference<?> removedRef = refQueue.remove(30_000L);
if (removedRef == null) {
throw new AssertionError("garbage collector did not reclaim the value");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.logging.log4j.core;

import static org.apache.logging.log4j.core.GcHelper.awaitGarbageCollection;

import org.junit.jupiter.api.Test;

class GcHelperTest {

@Test
void await_should_work() throws InterruptedException {
awaitGarbageCollection(Object::new);
}
}
Loading