RemoteAppServerConnectionPool.java

/*
** Module   : RemoteAppServerConnectionPool.java
** Abstract : A dedicated single-worker pool which will perform the actual request for remote connections.
**
** Copyright (c) 2022-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20220427 First version.
**     CA  20221031 Added 'broadcast' to send a message to all workers (like 'disconnect').
** 002 GBB 20240610 LegacyAppServerWork replaced by Consumer<LegacyServiceWorker>. 
** 003 GBB 20250403 Removing isSessionFree, since it's no longer determined by the appserver with MSA.
** 004 CA  20250324 Improvements for remote FWD OpenClient connections: ensure that the remote side uses a
**                  single connection to the FWD server, and also use a client-side pool of worker threads to
**                  send the requests (the pool size is in sync with the maximum agents available to process
**                  requests).
*/

/*
** 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.main;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;

/**
 * When establishing a remote appserver connection, to use the FWD's implementation of the OpenClient, a
 * single-worker pool is created.  This worker is not shared with other threads, it will establish a private
 * appserver connection to the FWD server, while sharing the underlying FWD server physical connection (in
 * more specific terms, the {@link AppServerEntry} network proxy will be shared).
 * <p>
 * In remote connection mode, the target request is executed immediately, but not on this thread - instead,
 * will be posted to a thread pool managed by {@link #executor} - this pool size is the same as the maximum
 * number of agents available on the remote FWD server, for that appserver.
 */
public class RemoteAppServerConnectionPool
extends AppServerConnectionPool
{
   /** Anonymous log instance. */
   protected static final CentralLogger LOG = CentralLogger.get(RemoteAppServerConnectionPool.class);
   
   /** The connected FWD servers on this client. */
   private static final Map<RemoteServerKey, RemoteServerInfo> connected = new HashMap<>();

   /** The pool of workers for a specific FWD server. */
   private static final Map<RemoteServerKey, Map<String, ExecutorService>> executors = new HashMap<>();
   
   /** The target appserver name. */
   private final String appserver;
   
   /** Flag indicating if the connection is in session-free mode. */
   private final boolean sessionFree;

   /** The worker which will execute the requests. */
   private final RemoteLegacyServiceWorker worker;

   /** The target FWD server. */
   private final RemoteServerInfo server;

   /** The executor for the work to be posted to the FWD server*/
   private ExecutorService executor;
   
   /**
    * Configure this remote connection for the specified appserver.
    * 
    * @param    cfg
    *           The configuration for the FWD server connection.
    * @param    appserver
    *           The appserver name.
    */
   public RemoteAppServerConnectionPool(BootstrapConfig cfg, String appserver)
   {
      this(cfg, appserver, true);
   }
   
   /**
    * Configure this remote connection for the specified appserver.
    * 
    * @param    cfg
    *           The configuration for the FWD server connection.
    * @param    appserver
    *           The appserver name.
    * @param    sessionFree
    *           Flag indicating if session-free mode is used.
    */
   public RemoteAppServerConnectionPool(BootstrapConfig cfg, String appserver, boolean sessionFree)
   {
      super("remoteFWDAppServerConnectionPool");
      
      this.appserver = appserver;
      this.sessionFree = sessionFree;
      
      RemoteServerKey serverKey = new RemoteServerKey(getServerConfigs(cfg));
      
      this.server = initializeServer(cfg, serverKey, appserver);
      this.executor = executors.get(serverKey).get(appserver);
      this.worker = new RemoteLegacyServiceWorker(this, server, appserver, true, sessionFree);
   }

   /**
    * Initialize the connection pool - no special code is required for remote connections.
    */
   @Override
   public void initialize()
   {
      initialized = true;
      
      // establish the connection now
      try
      {
         execute(() -> worker.getHelper());
      }
      catch (Throwable t)
      {
         if (t instanceof RuntimeException)
         {
            throw (RuntimeException) t;
         }
         
         throw new RuntimeException(t);
      }
   }
   
   /**
    * Code to process appserver requests.
    * <p>
    * In this mode, the request is posted to one of the {@link #executor} threads - this will use the 
    * {@link #worker}'s appserver connection to the FWD server, and will wait for the request to complete.
    * 
    * @param    connectionID
    *           When not-null, force to use the worker with the specified connection ID.
    * @param    work
    *           The work to perform the request.
    * @param    target
    *           The target path.
    *           
    * @return   <code>true</code> if the request was processed by a {@link #workers worker}.
    */
   @Override
   public boolean dispatch(String connectionID, LegacyAppServerWork work, String target)
   {
      try
      {
         execute(() -> 
         {
            try
            {
               work.accept(worker);
            }
            catch (Throwable t)
            {
               if (t instanceof RuntimeException)
               {
                  throw (RuntimeException) t;
               }
               
               throw new RuntimeException(t);
            }
         });
      }
      catch (Throwable t)
      {
         if (t instanceof RuntimeException)
         {
            throw (RuntimeException) t;
         }
         
         throw new RuntimeException(t);
      }

      return true;
   }
   
   /**
    * Broadcast a task to all the workers.
    * 
    * @param    work
    *           The work to perform the request.
    */
   @Override
   public void broadcast(LegacyAppServerWork work)
   {
      try
      {
         execute(() -> work.accept(worker));
      }
      catch (Throwable t)
      {
         if (t instanceof RuntimeException)
         {
            throw (RuntimeException) t;
         }
         
         throw new RuntimeException(t);
      }
   }
   
   /**
    * For remote connections, the worker has no dedicated thread, as the work is submitted to the shared
    * {@link #executor thread pool}, associated with the target {@link #server FWD server}.
    */
   @Override
   public void terminateWorkers()
   {
   }
   
   /**
    * Get the size of this pool.
    * 
    * @return   Always <code>1</code>.
    */
   @Override
   public int size()
   {
      return 1;
   }
   
   /**
    * Get the target appserver name.
    * 
    * @return   See above.
    */
   @Override
   public String getAppserver()
   {
      return appserver;
   }
   
   /**
    * Check if the appserver connection must be in session-free mode.
    * 
    * @return   See above.
    */
   @Override
   protected boolean isSessionFree()
   {
      return sessionFree;
   }

   /**
    * Initialize a connection to the FWD server given in the bootstrap configs.  If the FWD server is already
    * {@link #connected}, return the {@link RemoteServerInfo details}.
    * <p>
    * If this is also the first connection to the appserver, then an executor service will be created, to 
    * manage requests to the appserver, over the single {@link RemoteServerInfo#appServer FWD connection}.
    * 
    * @param    cfg
    *           The bootstrap configuration.
    * @param    key
    *           The key for the FWD server connection.
    * @param    appserver
    *           The appserver to connect.
    *           
    * @return   The details of the remote FWD server.
    */
   private static RemoteServerInfo initializeServer(BootstrapConfig cfg, 
                                                    RemoteServerKey key, 
                                                    String          appserver)
   {
      RemoteServerInfo server = null;
      
      synchronized (connected)
      {
         server = connected.get(key);
         if (server == null)
         {
            try
            {
               // create the SecurityManager
               SecurityManager.createInstance(cfg);
               // this node will act as a leaf node
               SessionManagerFactory.createLeafNode(cfg);
            }
            catch (NoSuchMethodException | 
                   ConfigurationException e)
            {
               LOG.log(Level.SEVERE, "Could not establish FWD connection.", e);
               return null;
            }

            server = AppServerHelper.connectToServer(cfg, null, null, null);
            if (server.connectError != null)
            {
               throw new RuntimeException(server.connectError);
            }
            connected.put(key, server);
            
            server.session.addSessionListener(new SessionListener()
            {
               
               @Override
               public void terminate(Session session)
               {
                  // cleanup if the FWD server disconnects
                  connected.remove(key);
                  Map<String, ExecutorService> services = executors.remove(key);
                  for (ExecutorService service : services.values())
                  {
                     service.shutdown();
                  }
               }
               
               @Override
               public void initialize(Session session)
               {
               }
            });
         }
         
         Map<String, ExecutorService> appExecutors = 
            executors.computeIfAbsent(key, (k) -> new ConcurrentHashMap<>());
         synchronized (appExecutors)
         {
            ExecutorService executor = appExecutors.get(appserver);
            if (executor == null) 
            {
               int threadNum = server.appServer.getRemotePoolThreads(appserver);
               AtomicInteger n = new AtomicInteger();
               executor = Executors.newFixedThreadPool(threadNum, 
               new ThreadFactory() 
               {
                  public Thread newThread(Runnable r) 
                  {
                     Thread t = Executors.defaultThreadFactory().newThread(r);
                     t.setName("Appserver <" + appserver + "> worker thread #" + n.incrementAndGet());
                     t.setDaemon(true);
                     return t;
                  }
               });
               
               appExecutors.put(appserver, executor);
            }
         }
      }
      
      return server;
   }
   
   /**
    * Get the minimal configurations which specify the details of the FWD server, from the given bootstrap
    * config instance.
    * 
    * @param    cfg
    *           The bootstrap configuration.
    *           
    * @return   See above.
    */
   private static Map<String, String> getServerConfigs(BootstrapConfig cfg)
   {
      try
      {
         Map<String, String> res = new HashMap<>();
         res.put("net:connection:secure", cfg.getConfigItem("net", "connection", "secure"));
         res.put("net:server:host", cfg.getConfigItem("net", "server", "host"));
         res.put("net:server:insecure_port", cfg.getConfigItem("net", "server", "insecure_port"));
         res.put("net:server:secure_port", cfg.getConfigItem("net", "server", "secure_port"));
         res.put("net:keystore:processalias", cfg.getConfigItem("net", "keystore", "processalias"));
         return res;
      }
      catch (ConfigurationException exc)
      {
         // should not happen, but throw anyway
         throw new RuntimeException(exc);
      }
   }
   
   /**
    * Execute the given task and wait the amount of {@link #getTimeout() seconds}.  If the timeout is zero,
    * then it will wait until the task completes.
    * 
    * @param    task
    *           The task to execute.
    * 
    * @throws   ExecutionException
    *           If the request execution failed. 
    */
   private void execute(Runnable task) throws ExecutionException
   {
      int timeout = getTimeout();
      try
      {
         Future future = executor.submit(task);
         if (timeout == 0)
         {
            future.get();
         }
         else
         {
            future.get(timeout, TimeUnit.SECONDS);
         }
      }
      catch (ExecutionException ee)
      {
         LOG.log(Level.SEVERE, "Request execution failed", ee);
         throw ee;
      }
      catch (InterruptedException | 
             TimeoutException e)
      {
         LOG.log(Level.WARNING, "Request interrupted after " + timeout + " seconds.");
      }
   }
   
   /**
    * The key with the details for a remote FWD server connection.
    */
   private static class RemoteServerKey
   {
      /** The bootstrap configurations used to establish the remote FWD server connection. */
      private final Map<String, String> configs;
      
      /** Pre-calculated hash-code. */
      private final int hashCode;
      
      /**
       * Create an instance with the specified FWD server configuration.
       * 
       * @param    configs
       *           The configuration for the FWD server connection.
       */
      public RemoteServerKey(Map<String, String> configs)
      {
         this.configs = Collections.unmodifiableMap(new HashMap<>(configs));
         this.hashCode = calculateHashCode();
      }
      
      /**
       * Get the {@link #hashCode}.
       * 
       * @return   See above.
       */
      @Override
      public int hashCode()
      {
         return hashCode;
      }
      
      /**
       * Check if the given instance is the same as this one.  This means that all the configs in current 
       * instance  must match the configs in the given instance (and vice-versa).
       * 
       * @param    obj
       *           The other instance to compare.
       *           
       * @return   <code>true</code> if the instance matches this one.
       */
      @Override
      public boolean equals(Object obj)
      {
         if (!(obj instanceof RemoteServerKey))
         {
            return false;
         }
         
         RemoteServerKey key = (RemoteServerKey) obj;
         Iterator<Map.Entry<String, String>> iter = this.configs.entrySet().iterator();
         while (iter.hasNext())
         {
            Map.Entry<String, String> e = iter.next();
            if (!(key.configs.containsKey(e.getKey()) && 
                  Objects.equals(e.getValue(), key.configs.get(e.getKey()))))
            {
               return false;
            }
         }
         
         return true;
      }
      
      /**
       * Calculate the hash-code from the given {@link #configs}.
       * 
       * @return   See above.
       */
      private int calculateHashCode()
      {
         int result = 17;
         result = 37 * result + Integer.hashCode(configs.size());
         Iterator<Map.Entry<String, String>> iter = this.configs.entrySet().iterator();
         while (iter.hasNext())
         {
            Map.Entry<String, String> e = iter.next();
            
            result = 37 * result + e.getKey().hashCode();
            result = 37 * result + Objects.hashCode(e.getValue());
         }
         
         return result;
      }
   }
}