001/* 002 * ============================================================================ 003 * Copyright © 2002-2026 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * 007 * Licensed to the public under the agreements of the GNU Lesser General Public 008 * License, version 3.0 (the "License"). You may obtain a copy of the License at 009 * 010 * http://www.gnu.org/licenses/lgpl.html 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 015 * License for the specific language governing permissions and limitations 016 * under the License. 017 */ 018 019package org.tquadrat.foundation.perflog.internal; 020 021import static java.math.RoundingMode.CEILING; 022import static java.util.concurrent.Executors.newScheduledThreadPool; 023import static javax.management.JMX.newMBeanProxy; 024import static org.apiguardian.api.API.Status.INTERNAL; 025import static org.tquadrat.foundation.lang.Objects.nonNull; 026import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 027import static org.tquadrat.foundation.value.Time.MILLISECOND; 028 029import javax.management.InstanceAlreadyExistsException; 030import javax.management.MBeanRegistrationException; 031import javax.management.MBeanServer; 032import javax.management.NotCompliantMBeanException; 033import javax.management.ObjectName; 034import java.lang.Thread.UncaughtExceptionHandler; 035import java.lang.ref.Cleaner; 036import java.lang.ref.Cleaner.Cleanable; 037import java.math.MathContext; 038import java.util.Optional; 039import java.util.concurrent.ScheduledExecutorService; 040import java.util.concurrent.ScheduledFuture; 041import java.util.concurrent.atomic.AtomicInteger; 042 043import org.apiguardian.api.API; 044import org.tquadrat.foundation.annotation.ClassVersion; 045import org.tquadrat.foundation.exception.ImpossibleExceptionError; 046import org.tquadrat.foundation.exception.UnexpectedExceptionError; 047import org.tquadrat.foundation.perflog.PerfLogMBean; 048import org.tquadrat.foundation.perflog.PerfLogManager; 049import org.tquadrat.foundation.perflog.PerformanceSection; 050import org.tquadrat.foundation.perflog.PerformanceSectionName; 051import org.tquadrat.foundation.perflog.PerformanceTracker; 052 053/** 054 * <p>{@summary The implementation for the interface 055 * {@link org.tquadrat.foundation.perflog.PerfLogManager}.}</p> 056 * 057 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 058 * @version $Id: PerfLogManagerImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 059 * @since 0.25.0 060 * 061 * @UMLGraph.link 062 */ 063@ClassVersion( sourceVersion = "$Id: PerfLogManagerImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 064@API( status = INTERNAL, since = "0.25.0" ) 065public final class PerfLogManagerImpl implements PerfLogManager 066{ 067 /*---------------*\ 068 ====** Inner Classes **==================================================== 069 \*---------------*/ 070 /** 071 * <p>{@summary The janitor that takes care of the housekeeping for an 072 * instance of 073 * {@link PerfLogManager} 074 * in case that was not properly closed.}</p> 075 * 076 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 077 * @version $Id: PerfLogManagerImpl.java 1258 2026-06-04 18:33:06Z tquadrat $ 078 * @since 0.25.0 079 * 080 * @param scheduledExecutorService The reference to the 081 * {@link ScheduledExecutorService} 082 * that needs to be terminated. 083 * 084 * @UMLGraph.link 085 */ 086 @SuppressWarnings( "NewClassNamingConvention" ) 087 @ClassVersion( sourceVersion = "$Id: PerfLogManagerImpl.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 088 @API( status = INTERNAL, since = "0.25.0" ) 089 private record Janitor( ScheduledExecutorService scheduledExecutorService ) implements Runnable 090 { 091 /*---------*\ 092 ====** Methods **====================================================== 093 \*---------*/ 094 /** 095 * {@inheritDoc} 096 */ 097 @Override 098 public final void run() 099 { 100 if( nonNull( scheduledExecutorService ) && !scheduledExecutorService.isTerminated() ) 101 { 102 scheduledExecutorService.shutdown(); 103 } 104 } // run() 105 } 106 // record Janitor 107 108 /*------------*\ 109 ====** Attributes **======================================================= 110 \*------------*/ 111 /** 112 * The 113 * {@link Cleanable} 114 * for this instance. 115 */ 116 private final Cleanable m_Cleanable; 117 118 /** 119 * The instance counter that is used for the thread names in 120 * {@link #createTimeoutMonitor(UncaughtExceptionHandler)}. 121 */ 122 private static final AtomicInteger m_InstanceCounter = new AtomicInteger( 0 ); 123 124 /** 125 * The flag the indicates whether this manager is (still) active. 126 */ 127 private boolean m_IsActive; 128 129 /** 130 * The caretaker for this instance. 131 */ 132 @SuppressWarnings( "FieldCanBeLocal" ) 133 private final Janitor m_Janitor; 134 135 /** 136 * The proxy for the 137 * {@link org.tquadrat.foundation.perflog.PerfLogMBean}. 138 */ 139 private final PerfLogMBean m_MBean; 140 141 /** 142 * The timeout scheduler for this manager. 143 */ 144 private final ScheduledExecutorService m_TimeoutScheduler; 145 146 /*------------------------*\ 147 ====** Static Initialisations **=========================================== 148 \*------------------------*/ 149 /** 150 * The cleaner that is used to finalise instances of 151 * {@code PerfLogManagerImpl}. 152 */ 153 private static final Cleaner m_Cleaner = Cleaner.create(); 154 155 /** 156 * The instance of 157 * {@link MathContext} 158 * that is used by 159 * {@link #registerTimeoutMonitor(PerformanceTrackerImpl)} 160 * to calculate the timeout waiting period from the given 161 * {@link org.tquadrat.foundation.value.TimeValue}. 162 */ 163 private static final MathContext MATH_CONTEXT = new MathContext( 32, CEILING ); 164 165 /*--------------*\ 166 ====** Constructors **===================================================== 167 \*--------------*/ 168 /** 169 * Creates a new instance of {@code PerfLogManagerImpl}. 170 * 171 * @param mbeanServer The MBean server that is used. 172 * @param objectName The name of the 173 * {@link PerfLogMBean}. 174 */ 175 public PerfLogManagerImpl( final MBeanServer mbeanServer, final ObjectName objectName ) 176 { 177 this( false, mbeanServer, objectName, null ); 178 } // PerfLogManagerImpl() 179 180 /** 181 * Creates a new instance of {@code PerfLogManagerImpl}. 182 * 183 * @param mbeanServer The MBean server that is used. 184 * @param objectName The name of the 185 * {@link PerfLogMBean}. 186 * @param uncaughtExceptionHandler The implementation of 187 * {@link Thread.UncaughtExceptionHandler} 188 * that is used for the timeout monitoring thread. 189 */ 190 @SuppressWarnings( "MethodParameterNamingConvention" ) 191 public PerfLogManagerImpl( final MBeanServer mbeanServer, final ObjectName objectName, final UncaughtExceptionHandler uncaughtExceptionHandler ) 192 { 193 this( false, mbeanServer, objectName, requireNonNullArgument( uncaughtExceptionHandler, "uncaughtExceptionHandler" ) ); 194 } // PerfLogManagerImpl() 195 196 /** 197 * Creates a new instance of {@code PerfLogManagerImpl}. 198 * 199 * @param ignored Discriminator 200 * @param mbeanServer The MBean server that is used. 201 * @param objectName The name of the 202 * {@link PerfLogMBean}. 203 * @param uncaughtExceptionHandler The implementation of 204 * {@link Thread.UncaughtExceptionHandler} 205 * that is used for the timeout monitoring thread. 206 */ 207 @SuppressWarnings( "MethodParameterNamingConvention" ) 208 private PerfLogManagerImpl( final boolean ignored, final MBeanServer mbeanServer, final ObjectName objectName, final UncaughtExceptionHandler uncaughtExceptionHandler ) 209 { 210 //---* Connect to the MBean *------------------------------------------ 211 m_MBean = connectToMBean( mbeanServer, objectName ); 212 213 //---* Create the timeout scheduler *---------------------------------- 214 m_TimeoutScheduler = createTimeoutMonitor( uncaughtExceptionHandler ); 215 216 //---* Register the timeout monitor for housekeeping *----------------- 217 m_Janitor = new Janitor( m_TimeoutScheduler ); 218 //noinspection ThisEscapedInObjectConstruction 219 m_Cleanable = m_Cleaner.register( this, m_Janitor ); 220 221 m_IsActive = true; 222 } // PerfLogManagerImpl() 223 224 /*---------*\ 225 ====** Methods **========================================================== 226 \*---------*/ 227 /** 228 * <p>{@summary Checks whether this performance logging manager is still 229 * active.} Throws an 230 * {@link IllegalStateException} 231 * if not. 232 * 233 * @return {@true} if the manager is still active. 234 * @throws IllegalStateException 235 * {@link #close()} 236 * was already called on this instance. 237 */ 238 @SuppressWarnings( "UnusedReturnValue" ) 239 private final boolean checkActive() throws IllegalStateException 240 { 241 if( !m_IsActive ) throw new IllegalStateException( "PerfLogManager was already terminated" ); 242 243 //---* Done *---------------------------------------------------------- 244 //noinspection ConstantValue 245 return m_IsActive; 246 } // checkActive() 247 248 /** 249 * {@inheritDoc} 250 */ 251 @Override 252 public final void close() 253 { 254 if( m_IsActive ) 255 { 256 m_Cleanable.clean(); 257 m_IsActive = false; 258 } 259 } // close() 260 261 /** 262 * <p>{@summary Establishes the connection with the 263 * {@link PerfLogMBean} 264 * on the given 265 * {@linkplain MBeanServer MBean server} 266 * and returns a proxy for it.}</p> 267 * <p>If the MBean had not been registered yet, this method will register 268 * it first.</p> 269 * 270 * @param mbeanServer The MBean server that is used. 271 * @param objectName The name for the MBean. 272 * @return A proxy for 273 * {@link PerfLogMBean}. 274 */ 275 private static final PerfLogMBean connectToMBean( final MBeanServer mbeanServer, final ObjectName objectName ) 276 { 277 final PerfLogMBean retValue; 278 if( !requireNonNullArgument( mbeanServer, "mbeanServer" ).isRegistered( requireNonNullArgument( objectName, "objectName" ) ) ) 279 { 280 final var mbean = new PerfLogMBeanImpl(); 281 try 282 { 283 mbeanServer.registerMBean( mbean, objectName ); 284 } 285 catch( final InstanceAlreadyExistsException _ ) 286 { 287 /* 288 * Someone else was faster to register the MBean. We ignore the 289 * exception and try to create the proxy. 290 */ 291 } 292 catch( final MBeanRegistrationException e ) 293 { 294 throw new UnexpectedExceptionError( e ); 295 } 296 catch( final NotCompliantMBeanException e ) 297 { 298 throw new ImpossibleExceptionError( e ); 299 } 300 } 301 retValue = newMBeanProxy( mbeanServer, objectName, PerfLogMBean.class ); 302 303 //---* Done *---------------------------------------------------------- 304 return retValue; 305 } // connectToMBean 306 307 /** 308 * {@inheritDoc} 309 */ 310 @Override 311 public final Optional<PerformanceTracker> createPerformanceTracker( final PerformanceSectionName name ) 312 { 313 checkActive(); 314 315 final var performanceSection = m_MBean.retrievePerformanceSection( requireNonNullArgument( name, "name" ) ); 316 317 final Optional<PerformanceTracker> retValue; 318 if( performanceSection.isIgnored() ) 319 { 320 retValue = Optional.empty(); 321 } 322 else 323 { 324 final var tracker = new PerformanceTrackerImpl( this, performanceSection ); 325 // TODO - Implement the registration for the timeout! 326 retValue = Optional.of( tracker ); 327 } 328 329 //---* Done *---------------------------------------------------------- 330 return retValue; 331 } // createPerformanceTracker() 332 333 /** 334 * Creates the 335 * {@link ScheduledExecutorService} 336 * instance that is used as the timeout monitor and registers it for the 337 * housekeeping. 338 * 339 * @param uncaughtExceptionHandler The implementation of 340 * {@link Thread.UncaughtExceptionHandler} 341 * that is used for the timeout monitoring threads. 342 * @return The timeout monitor. 343 */ 344 @SuppressWarnings( "MethodParameterNamingConvention" ) 345 private final ScheduledExecutorService createTimeoutMonitor( final UncaughtExceptionHandler uncaughtExceptionHandler ) 346 { 347 //---* Create the ScheduledExecutorService *--------------------------- 348 final var threadName = "TimeoutMonitor[%d]".formatted( m_InstanceCounter.incrementAndGet() ); 349 final var builder = Thread.ofVirtual() 350 .name( threadName ); 351 if( nonNull( uncaughtExceptionHandler ) ) builder.uncaughtExceptionHandler( uncaughtExceptionHandler); 352 final var retValue = newScheduledThreadPool( 1, builder.factory() ); 353 354 //---* Done *---------------------------------------------------------- 355 return retValue; 356 } // createTimeoutMonitor() 357 358 /** 359 * <p>{@summary Provides a reference to the internal instance of 360 * {@link PerfLogMBean}.}</p> 361 * 362 * @note This is the proxy, but <i>not</i> the MBean itself. 363 * @note The API is not limited to the OpenMBean API that is published to 364 * a remote client. 365 * 366 * @return The reference to the MBean. 367 */ 368 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 369 public final PerfLogMBean getMBean() { return m_MBean; } 370 371 /** 372 * {@inheritDoc} 373 */ 374 @Override 375 public final Optional<PerformanceSection> getPerformanceSection( final PerformanceSectionName name ) 376 { 377 checkActive(); 378 379 final var retValue = m_MBean.getPerformanceSection( requireNonNullArgument( name, "name" ) ); 380 381 //---* Done *---------------------------------------------------------- 382 return retValue; 383 } // getPerformanceSection() 384 385 /** 386 * {@inheritDoc} 387 */ 388 @SuppressWarnings( "NewMethodNamingConvention" ) 389 @Override 390 public void loadPerformanceSectionDefinitions( final Iterable<PerformanceSection> definitions ) 391 { 392 checkActive(); 393 394 for( final var definition : requireNonNullArgument( definitions, "definitions" ) ) 395 { 396 m_MBean.addPerformanceSection( definition ); 397 } 398 } // loadPerformanceSectionDefinitions 399 400 /** 401 * Registers a timeout monitor from a performance tracker. 402 * 403 * @param tracker The performance tracker to register. 404 * @return The timeout monitor; will be {@null} if the given tracker 405 * is not active, or the performance section for that tracker does 406 * not define a timeout value. 407 * @throws IllegalStateException 408 * {@link #close()} 409 * was already called on this instance. 410 */ 411 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 412 public final ScheduledFuture<?> registerTimeoutMonitor( final PerformanceTrackerImpl tracker ) throws IllegalStateException 413 { 414 checkActive(); 415 416 ScheduledFuture<?> retValue = null; 417 final var section = requireNonNullArgument( tracker, "tracker" ).getPerformanceSection(); 418 final var timeout = section.getTimeout(); 419 if( tracker.isActive() && timeout.isPresent() ) 420 { 421 final var unit = MILLISECOND; 422 final var timeoutValue = timeout.get().convert( unit ).round( MATH_CONTEXT ).longValue(); 423 retValue = m_TimeoutScheduler.schedule( () -> tracker.abort( true ), timeoutValue, unit.getTimeUnit() ); 424 } 425 426 //---* Done *---------------------------------------------------------- 427 return retValue; 428 } // registerTimeoutMonitor() 429 430 /** 431 * Sends a performance report to the 432 * {@link PerfLogMBean}. 433 * 434 * @param tracker The tracker that collected the performance data. 435 * @param message The message describing the reason for the abort of the 436 * tracker; can be {@null}. 437 * @param cause The exception that caused the abort; can be 438 * {@null}. 439 * @throws IllegalStateException 440 * {@link #close()} 441 * was already called on this instance. 442 */ 443 @SuppressWarnings( "PublicMethodNotExposedInInterface" ) 444 public final void sendReport( final PerformanceTrackerImpl tracker, final String message, final Throwable cause ) throws IllegalStateException 445 { 446 if( checkActive() && nonNull( tracker ) ) 447 { 448 final var report = new PerformanceReportImpl( tracker, message, cause ); 449 final var section = tracker.getPerformanceSection(); 450 final var sendReportForAbort = section.isSendingReportForAbort(); 451 @SuppressWarnings( "LocalVariableNamingConvention" ) 452 final var sendReportOnlyForThresholdExceeded = section.isSendingReportOnlyForExceededThreshold(); 453 final var isTimedOut = tracker.isTimedOut(); 454 final var isThresholdExceeded = tracker.isThresholdExceeded(); 455 final var elapsedTime = tracker.getElapsedTime(); 456 switch( tracker.getStatus() ) 457 { 458 case STATUS_ABORTED -> 459 { 460 if( isTimedOut || sendReportForAbort ) 461 { 462 m_MBean.receivePerformanceReport( report ); 463 } 464 } 465 466 case STATUS_STOPPED -> 467 { 468 if( elapsedTime.isPresent() && (!sendReportOnlyForThresholdExceeded || isThresholdExceeded) ) 469 { 470 m_MBean.receivePerformanceReport( report ); 471 } 472 } 473 474 default -> { /* Do nothing – although this case should never happen */ } 475 } 476 } 477 } // sendReport() 478} 479// class PerfLogManagerImpl 480 481/* 482 * End of File 483 */