View Javadoc
1   /*
2    * Copyright (C) 2012-2024 RRiBbit.org
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.rribbit.dispatching;
17  
18  import java.rmi.registry.LocateRegistry;
19  import java.rmi.registry.Registry;
20  import java.rmi.server.RMIClientSocketFactory;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.List;
24  import java.util.Random;
25  
26  import javax.rmi.ssl.SslRMIClientSocketFactory;
27  
28  import org.rribbit.Request;
29  import org.rribbit.Response;
30  import org.rribbit.processing.RmiRequestProcessor;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * This {@link RequestDispatcher} dispatches a {@link Request} to an {@link RmiRequestProcessor} via RMI. It offers automatic loadbalancing and failover.
36   * <p />
37   * 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}.
38   * This also means that, if you choose to use SSL, an SSL handshake is done on each request.
39   *
40   * @author G.J. Schouten
41   *
42   */
43  public class RmiRequestDispatcher implements RequestDispatcher {
44  
45  	private static final Logger log = LoggerFactory.getLogger(RmiRequestDispatcher.class);
46  
47  	protected int retryAttempts = 10;
48  	protected int portnumber;
49  	protected String[] hosts;
50  	protected String truststoreLocation;
51  
52  	/**
53  	 * 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,
54  	 * 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.
55  	 * <p />
56  	 * Use this constructor if you do NOT want to use SSL.
57  	 *
58  	 * @param portnumber			The portnumber to use
59  	 * @param hosts					The hosts to connect to, you can specify one host multiple times, to give it a proportionally larger chance of being chosen
60  	 */
61  	public RmiRequestDispatcher(int portnumber, String... hosts) {
62  
63  		this.portnumber = portnumber;
64  		this.hosts = hosts;
65  	}
66  
67  	/**
68  	 * 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,
69  	 * 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.
70  	 * <p />
71  	 * Use this constructor if you DO want to use SSL. The "javax.net.ssl.trustStore" system property will be set.
72  	 *
73  	 * @param truststoreLocation	The filepath that contains the SSL truststore, without file://
74  	 * @param portnumber			The portnumber to use
75  	 * @param hosts					The hosts to connect to, you can specify one host multiple times, to give it a proportionally larger chance of being chosen
76  	 */
77  	public RmiRequestDispatcher(String truststoreLocation, int portnumber, String... hosts) {
78  
79  		this.truststoreLocation = truststoreLocation;
80  		this.portnumber = portnumber;
81  		this.hosts = hosts;
82  
83  		System.setProperty("javax.net.ssl.trustStore", truststoreLocation);
84  	}
85  
86  	@Override
87  	public <T> Response<T> dispatchRequest(Request request) {
88  
89  		List<String> hostsAsList = new ArrayList<>(Arrays.asList(hosts));
90  
91  		log.info("Connecting to an RmiRequestProcessorImpl");
92  		for(int i=0; i<retryAttempts; i++) {
93  			log.info("Attempt: {} (Max Attempts: {})", i+1, retryAttempts);
94  
95  			//If all the available hosts have been tried, then start over
96  			if(hostsAsList.isEmpty()) {
97  				log.info("All hosts have been tried. Re-loading...");
98  				hostsAsList = new ArrayList<>(Arrays.asList(hosts));
99  			}
100 
101 			//Pick a host from the available host and remove it from the list
102 			int number = new Random().nextInt(hostsAsList.size());
103 			String host = hostsAsList.remove(number);
104 
105 			//Connect to the host and dispatch the Request
106 			try {
107 				log.info("Connecting to server: '{}:{}'", host, portnumber);
108 				Registry registry;
109 				if(truststoreLocation == null) { //No SSL
110 					registry = LocateRegistry.getRegistry(host, portnumber);
111 				} else { //SSL
112 					RMIClientSocketFactory csf = new SslRMIClientSocketFactory();
113 					registry = LocateRegistry.getRegistry(host, portnumber, csf);
114 				}
115 
116 				RmiRequestProcessor rmiRequestProcessor = (RmiRequestProcessor) (registry.lookup(RmiRequestProcessor.REGISTRY_KEY));
117 				Response<T> response = rmiRequestProcessor.processRequestViaRMI(request);
118 				log.info("Returning Response");
119 				return response;
120 			} catch(Exception e) {
121 				log.error("Connection failed, trying again...", e);
122 			}
123 		}
124 		log.error("No connection to an RmiRequestProcessorImpl could be made");
125 		throw new RuntimeException("No connection to an RmiRequestProcessorImpl could be made");
126 	}
127 
128 	/**
129 	 * Gets the number of times this {@link RmiRequestDispatcher} retries when it cannot connect to the {@link RmiRequestProcessor}. The default is 10.
130 	 */
131 	public int getRetryAttempts() {
132 		return retryAttempts;
133 	}
134 
135 	/**
136 	 * Sets the number of times this {@link RmiRequestDispatcher} retries when it cannot connect to the {@link RmiRequestProcessor}. The default is 10.
137 	 *
138 	 * @param retryAttempts
139 	 */
140 	public void setRetryAttempts(int retryAttempts) {
141 		this.retryAttempts = retryAttempts;
142 	}
143 }