/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2026 3A Systems, LLC. */ package org.opends.server.loggers; import static org.testng.Assert.*; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.server.config.server.DebugLogPublisherCfg; import org.opends.server.DirectoryServerTestCase; import org.opends.server.core.ServerContext; import org.testng.annotations.Test; @SuppressWarnings("javadoc") public class DebugLogPublisherTest extends DirectoryServerTestCase { /** Minimal publisher exposing nothing but the trace settings bookkeeping under test. */ private static final class TestDebugLogPublisher extends DebugLogPublisher { @Override public void initializeLogPublisher(DebugLogPublisherCfg config, ServerContext serverContext) { // Nothing to initialize. } @Override public void trace(TraceSettings settings, String signature, String sourceLocation, String msg, StackTraceElement[] stackTrace) { // Not used by these tests. } @Override public void traceException(TraceSettings settings, String signature, String sourceLocation, String msg, Throwable ex, StackTraceElement[] stackTrace) { // Not used by these tests. } @Override public DN getDN() { return null; } @Override public void close() { // Nothing to close. } } @Test public void testFreshPublisherTracesNothing() throws Exception { DebugLogPublisher publisher = new TestDebugLogPublisher(); assertSame(publisher.getClassSettings("com.example.Foo"), TraceSettings.DISABLED); assertNull(publisher.getMethodSettings("com.example.Foo")); assertFalse(publisher.hasTraceSettings("com.example.Foo")); assertFalse(publisher.hasTraceSettings("com.example.Foo#bar")); } @Test public void testGlobalSettingsOnlyApplyWhileNoTargetIsDefined() throws Exception { DebugLogPublisher publisher = new TestDebugLogPublisher(); TraceSettings global = new TraceSettings(); publisher.addTraceSettings(null, global); assertSame(publisher.getClassSettings("com.example.Foo"), global); publisher.addTraceSettings("com.example", new TraceSettings()); assertSame(publisher.getClassSettings("org.other.Bar"), TraceSettings.DISABLED); assertSame(publisher.removeTraceSettings(null), global); } @Test public void testMostSpecificClassScopeWins() throws Exception { DebugLogPublisher publisher = new TestDebugLogPublisher(); TraceSettings packageSettings = new TraceSettings(); TraceSettings classSettings = new TraceSettings(); publisher.addTraceSettings("com.example", packageSettings); publisher.addTraceSettings("com.example.Foo", classSettings); assertSame(publisher.getClassSettings("com.example.Foo"), classSettings); assertSame(publisher.getClassSettings("com.example.Foo$Inner"), classSettings); assertSame(publisher.getClassSettings("com.example.Bar"), packageSettings); assertSame(publisher.removeTraceSettings("com.example.Foo"), classSettings); assertSame(publisher.getClassSettings("com.example.Foo"), packageSettings); assertNull(publisher.removeTraceSettings("no.such.Scope")); } @Test public void testMethodSettings() throws Exception { DebugLogPublisher publisher = new TestDebugLogPublisher(); TraceSettings barSettings = new TraceSettings(); TraceSettings bazSettings = new TraceSettings(); publisher.addTraceSettings("com.example.Foo#bar", barSettings); publisher.addTraceSettings("com.example.Foo#baz", bazSettings); Map methodSettings = publisher.getMethodSettings("com.example.Foo"); assertNotNull(methodSettings); assertEquals(methodSettings.size(), 2); assertSame(methodSettings.get("bar"), barSettings); assertSame(methodSettings.get("baz"), bazSettings); assertTrue(publisher.hasTraceSettings("com.example.Foo#bar")); assertFalse(publisher.hasTraceSettings("com.example.Foo#unknown")); assertNull(publisher.removeTraceSettings("com.example.Foo#unknown")); assertNull(publisher.removeTraceSettings("no.such.Class#bar")); } @Test public void testRemovingLastMethodSettingsDiscardsTheEnclosingMap() throws Exception { DebugLogPublisher publisher = new TestDebugLogPublisher(); TraceSettings barSettings = new TraceSettings(); TraceSettings bazSettings = new TraceSettings(); publisher.addTraceSettings("com.example.Foo#bar", barSettings); publisher.addTraceSettings("com.example.Foo#baz", bazSettings); assertSame(publisher.removeTraceSettings("com.example.Foo#bar"), barSettings); assertNotNull(publisher.getMethodSettings("com.example.Foo")); assertSame(publisher.removeTraceSettings("com.example.Foo#baz"), bazSettings); assertNull(publisher.getMethodSettings("com.example.Foo")); } /** * The trace settings are read without any lock, so adding and removing method settings must be * atomic with respect to each other: dropping the map holding the settings of a class must not * discard a setting added concurrently for another method of the same class. */ @Test public void testConcurrentUpdatesDoNotLoseMethodSettings() throws Exception { final DebugLogPublisher publisher = new TestDebugLogPublisher(); final int writerCount = 4; final int readerCount = 2; final int rounds = 5000; final CountDownLatch start = new CountDownLatch(1); final CountDownLatch finished = new CountDownLatch(writerCount + readerCount); final AtomicReference failure = new AtomicReference<>(); for (int i = 0; i < writerCount; i++) { final String methodName = "method" + i; new Thread(() -> { try { start.await(); for (int round = 0; round < rounds; round++) { TraceSettings settings = new TraceSettings(); publisher.addTraceSettings("com.example.Hot#" + methodName, settings); Map methodSettings = publisher.getMethodSettings("com.example.Hot"); assertNotNull(methodSettings, "the map holding " + methodName + " was discarded"); assertSame(methodSettings.get(methodName), settings, "the settings of " + methodName + " were lost"); publisher.removeTraceSettings("com.example.Hot#" + methodName); } } catch (Throwable t) { failure.compareAndSet(null, t); } finally { finished.countDown(); } }).start(); } for (int i = 0; i < readerCount; i++) { new Thread(() -> { try { start.await(); for (int round = 0; round < rounds; round++) { publisher.getClassSettings("com.example.Hot$Inner"); Map methodSettings = publisher.getMethodSettings("com.example.Hot"); if (methodSettings != null) { for (TraceSettings settings : methodSettings.values()) { assertNotNull(settings.getLevel()); } } publisher.hasTraceSettings("com.example.Hot#method0"); } } catch (Throwable t) { failure.compareAndSet(null, t); } finally { finished.countDown(); } }).start(); } start.countDown(); finished.await(); if (failure.get() != null) { throw new AssertionError("concurrent access to the trace settings failed", failure.get()); } assertNull(publisher.getMethodSettings("com.example.Hot")); } }