1 /*
2 * Copyright (C) 2012-2025 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 }