RmiRequestDispatcher.java

/*
 * Copyright (C) 2012-2024 RRiBbit.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.rribbit.dispatching;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import javax.rmi.ssl.SslRMIClientSocketFactory;

import org.rribbit.Request;
import org.rribbit.Response;
import org.rribbit.processing.RmiRequestProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This {@link RequestDispatcher} dispatches a {@link Request} to an {@link RmiRequestProcessor} via RMI. It offers automatic loadbalancing and failover.
 * <p />
 * Note that the RMI connection is NOT maintained between requests, meaning that a new connection is set up each time a request is made to this {@link RmiRequestDispatcher}.
 * This also means that, if you choose to use SSL, an SSL handshake is done on each request.
 *
 * @author G.J. Schouten
 *
 */
public class RmiRequestDispatcher implements RequestDispatcher {

	private static final Logger log = LoggerFactory.getLogger(RmiRequestDispatcher.class);

	protected int retryAttempts = 10;
	protected int portnumber;
	protected String[] hosts;
	protected String truststoreLocation;

	/**
	 * Connects to {@link RmiRequestProcessor}s that run on the specified port and on the specified hosts. If multiple hosts are specified, one is randomly chosen at runtime,
	 * creating automatic load-balancing. If a connection to a host fails, another one is chosen. The default number of retries is 10 and can be set with the corresponding setter.
	 * <p />
	 * Use this constructor if you do NOT want to use SSL.
	 *
	 * @param portnumber			The portnumber to use
	 * @param hosts					The hosts to connect to, you can specify one host multiple times, to give it a proportionally larger chance of being chosen
	 */
	public RmiRequestDispatcher(int portnumber, String... hosts) {

		this.portnumber = portnumber;
		this.hosts = hosts;
	}

	/**
	 * Connects to {@link RmiRequestProcessor}s that run on the specified port and on the specified hosts. If multiple hosts are specified, one is randomly chosen at runtime,
	 * creating automatic load-balancing. If a connection to a host fails, another one is chosen. The default number of retries is 10 and can be set with the corresponding setters.
	 * <p />
	 * Use this constructor if you DO want to use SSL. The "javax.net.ssl.trustStore" system property will be set.
	 *
	 * @param truststoreLocation	The filepath that contains the SSL truststore, without file://
	 * @param portnumber			The portnumber to use
	 * @param hosts					The hosts to connect to, you can specify one host multiple times, to give it a proportionally larger chance of being chosen
	 */
	public RmiRequestDispatcher(String truststoreLocation, int portnumber, String... hosts) {

		this.truststoreLocation = truststoreLocation;
		this.portnumber = portnumber;
		this.hosts = hosts;

		System.setProperty("javax.net.ssl.trustStore", truststoreLocation);
	}

	@Override
	public <T> Response<T> dispatchRequest(Request request) {

		List<String> hostsAsList = new ArrayList<>(Arrays.asList(hosts));

		log.info("Connecting to an RmiRequestProcessorImpl");
		for(int i=0; i<retryAttempts; i++) {
			log.info("Attempt: {} (Max Attempts: {})", i+1, retryAttempts);

			//If all the available hosts have been tried, then start over
			if(hostsAsList.isEmpty()) {
				log.info("All hosts have been tried. Re-loading...");
				hostsAsList = new ArrayList<>(Arrays.asList(hosts));
			}

			//Pick a host from the available host and remove it from the list
			int number = new Random().nextInt(hostsAsList.size());
			String host = hostsAsList.remove(number);

			//Connect to the host and dispatch the Request
			try {
				log.info("Connecting to server: '{}:{}'", host, portnumber);
				Registry registry;
				if(truststoreLocation == null) { //No SSL
					registry = LocateRegistry.getRegistry(host, portnumber);
				} else { //SSL
					RMIClientSocketFactory csf = new SslRMIClientSocketFactory();
					registry = LocateRegistry.getRegistry(host, portnumber, csf);
				}

				RmiRequestProcessor rmiRequestProcessor = (RmiRequestProcessor) (registry.lookup(RmiRequestProcessor.REGISTRY_KEY));
				Response<T> response = rmiRequestProcessor.processRequestViaRMI(request);
				log.info("Returning Response");
				return response;
			} catch(Exception e) {
				log.error("Connection failed, trying again...", e);
			}
		}
		log.error("No connection to an RmiRequestProcessorImpl could be made");
		throw new RuntimeException("No connection to an RmiRequestProcessorImpl could be made");
	}

	/**
	 * Gets the number of times this {@link RmiRequestDispatcher} retries when it cannot connect to the {@link RmiRequestProcessor}. The default is 10.
	 */
	public int getRetryAttempts() {
		return retryAttempts;
	}

	/**
	 * Sets the number of times this {@link RmiRequestDispatcher} retries when it cannot connect to the {@link RmiRequestProcessor}. The default is 10.
	 *
	 * @param retryAttempts
	 */
	public void setRetryAttempts(int retryAttempts) {
		this.retryAttempts = retryAttempts;
	}
}