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 */