FwdServerJMX.java

/*
** Module   : FwdServerJMX.java
** Abstract : FWD JMX instrumentation. 
**
** Copyright (c) 2024-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 HC  20240222 Created initial version.
** 002 SP  20240731 Added JMX_DEBUG flag.
** 003 HC  20250205 Removed the disabled SPREADSHEET caching logic.
** 004 ICP 20241114 Added QueryCounter JMX.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.jmx;

import java.io.*;
import java.lang.management.*;
import java.util.*;
import java.util.function.Function;
import java.util.logging.*;
import javax.management.*;

import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;

/** FWD JMX instrumentation */
public class FwdServerJMX
extends FwdJMX
{
   /** When {@code true}, enable usage of some MBeans */
   public static final boolean JMX_DEBUG = System.getProperty("fwd_jmx_debug") != null;

   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(FwdServerJMX.class.getName());

   /**
    * Default constructor
    */
   private FwdServerJMX()
   {
      super("FWD Server");
   }

   /**
    * Register MBeans
    */
   public void registerMBeans()
   {
      try
      {
         MBeanServer server = ManagementFactory.getPlatformMBeanServer();
         for (TimeStat ts: TimeStat.values())
         {
            server.registerMBean(ts.getCounter(),
                                 new ObjectName("com.goldencode.p2j:name=" + ts.name()));
         }
         for (Counter c: Counter.values())
         {
            server.registerMBean(c.getCounter(),
                                 new ObjectName("com.goldencode.p2j:name=" + c.name()));
         }
         for (MapCounter c: MapCounter.values())
         {
            server.registerMBean(c.getCounter(), new ObjectName("com.goldencode.p2j:name=" + c.name()));
         }
         for (ThreadsCpuTimers ts: ThreadsCpuTimers.values())
         {
            server.registerMBean(ts.getCounter(), new ObjectName("com.goldencode.p2j:name=" + ts.name()));
         }
         for (QueryProfiler ts: QueryProfiler.values())
         {
            server.registerMBean(ts.getProfiler(), new ObjectName("com.goldencode.p2j:name=" + ts.name()));
         }
         for (QueryCounterEnum qc: QueryCounterEnum.values())
         {
            server.registerMBean(qc.getQueryCounter(), new ObjectName("com.goldencode.p2j:name=" + qc.name()));
         }
         server.registerMBean(this, new ObjectName("com.goldencode.p2j:name=All"));
      }
      catch (Exception e)
      {
         LOG.log(Level.SEVERE, "MBean registration failed", e);
      }
   }

   /**
    * Initialize singleton
    */
   public static void init()
   {
      Holder.INSTANCE.registerMBeans();
   }
   
   /**
    * Register MBean
    * 
    * @param mbean
    *        MBean to be registered 
    * @param name
    *        MBean name 
    */
   public static void register(Object mbean, String name)
   {
      Holder.INSTANCE.registerMBean(mbean, name);
   }

   /**
    * Instance holder (Lazy initialization)
    */
   private static class Holder
   {
      static final FwdServerJMX INSTANCE = new FwdServerJMX();
   }

   /**
    * Time counters
    */
   public static enum TimeStat
   implements OptionalCounter
   {
      /** ServerStat.widgetAttrs (de)serialization */
      ServerStateS11n,
      /** Message.payload (de)serialization */
      MessagePayloadS11n,
      /** Protocol.objToByteArray */
      objToByteArray,
      /** SSL send time */
      sslSend,
      /** SSL wrap time */
      sslWrap,
      /** SSL wrap time */
      sslUnwrap,

      /** Instrumentation for <code>ContextLocal.get</code>. */
      ContextLocalGet,

      /** Finer instrumentation for preparing the SymbolResolver in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_SymRes,
      /** Finer instrumentation for preparing the SchemaDictionary in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_SchemaDict,
      /** Finer instrumentation for preparing the lexer and parser in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Parse,
      /** Finer instrumentation for running ONLY the parser in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Parse2,
      /** Instrumentation for setting up for TRPL processing in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Setup,
      /** Instrumentation for running ONLY the annotations TRPL in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Annotation,
      /** Instrumentation for running ONLY the base_structure TRPL in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Base,
      /** Instrumentation for running ONLY the core_conversion TRPL in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Core,
      /** Instrumentation for running ONLY the postprocessing TRPL in {@code DynamicQueryHelper.parse()}. */
      OrmDynQueryProcess_Post,
      /** Instrumentation for optimizing the result JAST by interning the {@code String}-s in
       *  {@code DynamicQueryHelper.parse()}. They proved to have a low impact on performance. */
      OrmDynQueryProcess_Intern,

      /** Instrumentation for <code>DynamicQueryHelper.parseFindQuery</code>. */
      OrmDynParseFind,
      /** Instrumentation for <code>DynamicQueryHelper.parseQuery</code>. */
      OrmDynParseQuery,
      /** Instrumentation for <code>SQLQuery.hydrateRecord</code>. */
      OrmHydrateRecord,
      /** Instrumentation for <code>Query.createSqlQuery</code>*/
      OrmFqlParse,
      /** Instrumentation for <code>UnclosablePreparedStatement</code> execute methods. */
      OrmTempTableQuery,
      /** Instrumentation for <code>RuntimeJastInterpreter.interpret</code>. */
      OrmDynQueryInterpret,
      /** Instrumentation for <code>HQLHelper.obtain</code>. */
      OrmHqlHelperParse,
      /** Instrumentation for <code>PreselectQuery.assembleHQL</code>. */
      OrmPreselectQueryAssemble,
      /** Instrumentation for <code>TempTableBuilder.tempTablePrepareImpl</code>. */
      OrmCreateDynamicTable,
      /** Instrumentation for <code>TemporaryBuffer.createDynamicBufferForTempTable</code>. */
      OrmCreateDynamicBuffer,
      /** Instrumentation for <code>TemporaryBuffer.define</code>. */
      OrmBufferDefineTemp,
      /** Instrumentation for <code>RecordBuffer.define</code>. */
      OrmBufferDefinePerm,
      /** Instrumentation for <code>BufferImpl.resourceDelete</code>. */
      OrmBufferDelete,
      /** Instrumentation for <code>TableMapper.mapTemporaryTable</code>. */
      OrmTableMapperMap,


      /** Instrumentation for <code>TemporaryBuffer.associate</code>. */
      OrmTempTableParam,
      /** Instrumentation for <code>TemporaryBuffer.createDynamicTable</code>. */
      OrmDynamicTableParam,
      /** Instrumentation for <code>DataSet.associate</code>. */
      OrmDataSetParam,
      /** Instrumentation for <code>DataSet.createDynamicDataSet</code>. */
      OrmDynamicDataSetParam,

      /** Instrumentation for <code>OutputTableCopier.finished</code> */
      OutputTableCopier,
      /** Instrumentation for <code>OutputTableHandleCopier.finished</code> */
      OutputTableHandleCopier,
      /** Instrumentation for <code>OutputDataSetCopier.finished</code> */
      OutputDataSetCopier,
      /** Instrumentation for <code>OutputDataSetHandleCopier.finished</code> */
      OutputDataSetHandleCopier,

      /** Instrumentation for <code>TransactionManager.processCommit</code>. */
      TMCommit,
      /** Instrumentation for <code>TransactionManager.processRollback</code>. */
      TMRollback,
      /** Instrumentation for <code>TransactionManager.processValidate</code>. */
      TMValidate,

      /** Instrumentation for <code>LegacyJavaAppserver.addTableMetaData</code>. */
      AppserverAddTableMetaData,
      /** Instrumentation for <code>LegacyJavaAppserver.getJavaParameter</code>. */
      ApperverGetJavaParameter,
      /** Instrumentation for <code>LegacyJavaAppserver.getOutputParameter</code>. */
      AppserverGetOutputParameter;

      /** Counter instance */
      private final NanoCounter counter = new NanoCounter();

      /**
       * Register enable/disable setter
       * @param agent
       *        enable/disable setter
       */
      @Override
      public void register(BooleanConsumer agent)
      {
         counter.register(agent);
      }

      /**
       * Update counter
       * @param value
       *        new value
       */
      @Override
      public void update(long value)
      {
         counter.update(value);
      }

      /**
       * Return the string representation of the counter
       *
       * @param    ts
       *           The measurement time stamp
       * @param    comment
       *           The comment
       *
       * @return   The string representation of the counter
       *
       * @throws   IOException
       *           If IO exception occurs
       */
      @Override
      public String toString(String ts, String comment) throws IOException
      {
         return counter.toString();
      }

       /**
       * Return counter instance
       * @return counter instance
       */
      public NanoCounter getCounter()
      {
         return counter;
      }
   }

   /**
    * Defines the threads cpu counters.
    */
   public static enum ThreadsCpuTimers
   implements ThreadsCpuMBean
   {
      /** This counter is still unused */
      ThreadsCpuUtilization
      ;

      /** Counter instance */
      private final ThreadsCpuCounter counter = new ThreadsCpuCounter();

      /**
       * Register enable/disable setter.
       *
       * @param   agent
       *          enable/disable setter
       */
      @Override
      public void register(BooleanConsumer agent)
      {
         counter.register(agent);
      }

      /**
       * Update counter.
       *
       * @param    value
       *           new value
       */
      @Override
      public void update(long value)
      {
         counter.update(value);
      }

      /**
       * Return the string representation of the counter
       *
       * @param    ts
       *           The measurement time stamp
       * @param    comment
       *           The comment
       *
       * @return   The string representation of the counter
       *
       * @throws   IOException
       *           If IO exception occurs
       */
      @Override
      public String toString(String ts, String comment) throws IOException
      {
         return counter.toString(ts, comment);
      }

       /**
       * Return counter instance.
       *
       * @return   The counter instance
       */
      public ThreadsCpuMBean getCounter()
      {
         return counter;
      }

      /**
       * Gets the elapsed CPU time for the execution thread.
       *
       * @return   The total CPU time in nanoseconds spend by the execution thread.
       */
      @Override
      public long getThreadElapsedCpuTime()
      {
         return counter.getThreadElapsedCpuTime();
      }

      /**
       * Gets the elapsed CPU time for the given thread.
       *
       * @param    tid
       *           The given thread id
       *
       * @return   The total CPU time in nanoseconds spend by the given thread.
       */
      @Override
      public long getThreadElapsedCpuTime(long tid)
      {
         return counter.getThreadElapsedCpuTime(tid);
      }

      /**
       * Updates the elapsed CPU time spent the given thread by adding the given value.
       *
       * @param    tid
       *           The given thread id
       * @param    value
       *           The given value
       */
      @Override
      public void updateThreadElapsedCpuTime(long tid, long value)
      {
         counter.updateThreadElapsedCpuTime(tid, value);
      }

      /**
       * Gets the elapsed CPU time for the execution thread in a user mode.
       *
       * @return   The CPU time in nanoseconds spent by the execution thread in a user mode.
       */
      @Override
      public long getThreadElapsedUserTime()
      {
         return counter.getThreadElapsedUserTime();
      }

      /**
       * Gets the elapsed CPU time for the given thread in a user mode.
       *
       * @param    tid
       *           The given thread id
       *
       * @return   The CPU time in nanoseconds spent by the given thread in a user mode.
       */
      @Override
      public long getThreadElapsedUserTime(long tid)
      {
         return counter.getThreadElapsedUserTime(tid);
      }

      /**
       * Updates the elapsed CPU time spent the given thread in a user mode by adding the given value.
       *
       * @param    tid
       *           The given thread id
       * @param    value
       *           The given value
       */
     @Override
      public void updateThreadElapsedUserTime(long tid, long value)
      {
         counter.updateThreadElapsedUserTime(tid, value);
      }

     /**
      * Sets the execution thread.
      *
      * @param    tid
      *           The thread id
      */
      @Override
      public void setCurrentThread(long tid)
      {
         counter.setCurrentThread(tid);
      }
   }

   /**
    * Long values' counter
    */
   public static enum Counter
   implements OptionalCounter
   {
      /** Incomning network traffic */
      NetworkReads,
      /** Outgoing network traffic */
      NetworkWrites,

      /** Widget attributes flush count */
      WidgetAttrFlushes;

      /** Counter instance */
      private final SimpleLongCounter counter = new SimpleLongCounter();

      /**
       * Register enable/disable setter
       * @param agent
       *        enable/disable setter
       */
      @Override
      public void register(BooleanConsumer agent)
      {
         counter.register(agent);
      }

      /**
       * Update counter
       * @param value
       *        new value
       */
      @Override
      public void update(long value)
      {
         counter.update(value);
      }

      /**
       * Return the string representation of the counter
       *
       * @param ts
       *        The measurement time stamp
       * @param comment
       *        comment
       * @return string representation of the counter
       * @throws IOException
       *         If IO exception occurs
       */
      public String toString(String ts, String comment) throws IOException
      {
         return counter.toString();
      }

      /**
       * Return counter instance
       * @return counter instance
       */
     public SimpleLongCounter getCounter()
      {
         return counter;
      }
   }

   /**
    * String-long map counter
    */
   public static enum MapCounter
   implements OptionalMapCounter
   {
      /** Widget attribute enqueue count */
      WidgetAttrEnqueues,

      /** Task counter for each legacy service worker thread. */
      LegacyServiceWorkerTaskCounter,

      /** Spreadsheet command invocation nano times */
      SpreadsheetCmdNanos;

      /** Counter instance */
      private final SimpleMapCounter counter = new SimpleMapCounter();

      /**
       * Register enable/disable setter
       * @param agent
       *        enable/disable setter
       */
      @Override
      public void register(BooleanConsumer agent)
      {
         counter.register(agent);
      }

      /**
       * Update counter
       *
       * @param valueName
       *        value name to update
       * @param value
       *        new value
       */
      @Override
      public void update(String valueName, long value)
      {
         counter.update(valueName, value);
      }

      /**
       * Return the string representation of the counter
       * 
       * @param    ts
       *           The measurement time stamp
       * @param    comment
       *           The comment
       * 
       * @return   The string representation of the counter
       * 
       * @throws   IOException
       *           If IO exception occurs
       */
      public String toString(String ts, String comment) throws IOException
      {
         return counter.toString();
      }

      /**
       * Return counter instance
       * @return counter instance
       */
      public SimpleMapCounter getCounter()
      {
         return counter;
      }
   }

   /**
    * Profiler which is able to count number of cache misses and hits of each executed query
    */
   public static enum QueryProfiler
   implements OptionalQueryProfiler
   {
      /** Query profiler */
      QueryProfiler;

      /** Profiler instance */
      private final SimpleQueryProfiler profiler = new SimpleQueryProfiler();

      /**
       * Register enable/disable setter
       * @param agent
       *        enable/disable setter
       */
      @Override
      public void register(BooleanConsumer agent)
      {
         profiler.register(agent);
      }

      /**
       * Retrieve the profiler back-end of this profiler.
       *
       * @return   The simple query profiler
       */
      public SimpleQueryProfiler getProfiler()
      {
         return profiler;
      }

      /**
       * Update the number of rows retrieved from a result set
       *
       * @param query
       *        the query for which the update is done
       * @param value
       *        new value
       */
      @Override
      public void updateRowsCount(String query, long value)
      {
         profiler.updateRowsCount(query, value);

      }

      /**
       * Update the number of cache hits of a specified query
       *
       * @param query
       *        the query for which the update is done
       * @param value
       *        the number of cache hits
       */
      @Override
      public void updateCacheHits(String query, long value)
      {
         profiler.updateCacheHits(query, value);

      }

      /**
       * Update the number of cache misses of a specified query
       *
       * @param query
       *        the query for which the update is done
       * @param value
       *        the number of cache misses
       */
      @Override
      public void updateCacheMisses(String query, long value)
      {
         profiler.updateCacheMisses(query, value);

      }

      /**
       * Update the number of nanoseconds spent in hydrate of a specified query
       *
       * @param query
       *        the query for which the update is done
       * @param value
       *        the number of nanoseconds spent in hydrate
       */
      @Override
      public void updateHydrateTotalTime(String query, long value)
      {
         profiler.updateHydrateTotalTime(query, value);
      }

      /**
       * Return the string representation of a profiled query
       *
       * @param    ts
       *           The measurement time stamp
       * @param    comment
       *           The comment
       *
       * @return   The string representation of a profiled query
       */
      public String toString(String ts, String comment)
      {
         return profiler.toString();
      }
   }

   /**
    * Counter for executed queries
    */
   public static enum QueryCounterEnum
   implements QueryCounter
   {
      /** Query counter */
      QueryCounter;

      /** Counter instance */
      private final SimpleQueryCounter queryCounter = new SimpleQueryCounter();

      /**
       * Retrieve the counter back-end of this counter.
       *
       * @return   The simple query counter
       */
      public SimpleQueryCounter getQueryCounter()
      {
         return queryCounter;
      }

      /**
       * Update the query counting configuration for a specific database.
       *
       * @param   databaseName
       *          The database to update the configuration for.
       * @param   config
       *          The new configuration object.
       */
      @Override
      public void updateConfiguration(String databaseName, QueryCountConfiguration config)
      {
         queryCounter.updateConfiguration(databaseName, config);
      }

      /**
       * Updates the count for a specific query executed on the specified database.
       *
       * @param   databaseName
       *          The name of the database on which the query was executed.
       * @param   sqlInfo
       *          The object containing the sql query informations.
       */
      @Override
      public void updateCount(String databaseName, QueryInfo sqlInfo)
      {
         queryCounter.updateCount(databaseName, sqlInfo);
      }

      /**
       * Bootstrap method to register all databases with the query counter.
       *
       * @param   databases
       *          The list of database names to register.
       */
      @Override
      public void bootstrap(List<String> databases)
      {
         queryCounter.bootstrap(databases);
      }

      /**
       * Updates the supplier for initial configuration.
       *
       * @param   supplier
       *          The function to call to get the initial configuration.
       */
      public void updateSupplier(Function<String, QueryCountConfiguration> supplier)
      {
         queryCounter.updateSupplier(supplier);
      }
   }

   /**
    * Print all counters' values to the file.
    *
    * @param   file 
    *          The output file name (will be appended if exists).
    *
    * @throws  IOException 
    *          If IO exception occurs
    */
   @Override
   public synchronized void dumpAll(String file) throws IOException
   {
      String annotation = getAnnotation();

      for(TimeStat ts: TimeStat.values())
      {
         ts.getCounter().print(annotation + ":" + ts.name(), file);
      }
      for(Counter c: Counter.values())
      {
         c.getCounter().print(annotation + ":" + c.name(), file);
      }
      for(MapCounter c: MapCounter.values())
      {
         c.getCounter().print(annotation + ":" + c.name(), file);
      }
      for(QueryProfiler p: QueryProfiler.values())
      {
         p.getProfiler().print(file);
      }
   }
   
   /** Reset all counters */
   @Override
   public synchronized void resetAll()
   {
      Arrays.stream(TimeStat.values()).map(TimeStat::getCounter).forEach(NanoCounter::reset);
      Arrays.stream(Counter.values()).map(Counter::getCounter).forEach(SimpleLongCounter::reset);
      Arrays.stream(MapCounter.values()).map(MapCounter::getCounter).forEach(SimpleLongCounter::reset);
      Arrays.stream(QueryProfiler.values()).map(QueryProfiler::getProfiler).forEach(SimpleQueryProfiler::reset);
   }
   
   /**
    * Enable/disable all counters
    * @param val
    *        new value of the "enable" flag
    */
   @Override
   public synchronized void enableAll(boolean val)
   {
      Arrays.stream(TimeStat.values()).map(TimeStat::getCounter).forEach(c -> c.enable(val));
      Arrays.stream(Counter.values()).map(Counter::getCounter).forEach(c -> c.enable(val));
      Arrays.stream(MapCounter.values()).map(MapCounter::getCounter).forEach(c -> c.enable(val));
      Arrays.stream(QueryProfiler.values()).map(QueryProfiler::getProfiler).forEach(c -> c.enable(val));
   }
}