1 | /* |
---|
2 | Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved. |
---|
3 | |
---|
4 | The MySQL Connector/J is licensed under the terms of the GPLv2 |
---|
5 | <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors. |
---|
6 | There are special exceptions to the terms and conditions of the GPLv2 as it is applied to |
---|
7 | this software, see the FLOSS License Exception |
---|
8 | <http://www.mysql.com/about/legal/licensing/foss-exception.html>. |
---|
9 | |
---|
10 | This program is free software; you can redistribute it and/or modify it under the terms |
---|
11 | of the GNU General Public License as published by the Free Software Foundation; version 2 |
---|
12 | of the License. |
---|
13 | |
---|
14 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; |
---|
15 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
---|
16 | See the GNU General Public License for more details. |
---|
17 | |
---|
18 | You should have received a copy of the GNU General Public License along with this |
---|
19 | program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth |
---|
20 | Floor, Boston, MA 02110-1301 USA |
---|
21 | |
---|
22 | */ |
---|
23 | package com.mysql.jdbc; |
---|
24 | |
---|
25 | import java.lang.reflect.Constructor; |
---|
26 | import java.lang.reflect.InvocationHandler; |
---|
27 | import java.lang.reflect.InvocationTargetException; |
---|
28 | import java.lang.reflect.Method; |
---|
29 | import java.lang.reflect.Proxy; |
---|
30 | import java.sql.SQLException; |
---|
31 | import java.util.ArrayList; |
---|
32 | import java.util.Collections; |
---|
33 | import java.util.HashMap; |
---|
34 | import java.util.Iterator; |
---|
35 | import java.util.LinkedList; |
---|
36 | import java.util.List; |
---|
37 | import java.util.Map; |
---|
38 | import java.util.Properties; |
---|
39 | import java.util.Set; |
---|
40 | |
---|
41 | /** |
---|
42 | * An implementation of java.sql.Connection that load balances requests across a |
---|
43 | * series of MySQL JDBC connections, where the balancing takes place at |
---|
44 | * transaction commit. |
---|
45 | * |
---|
46 | * Therefore, for this to work (at all), you must use transactions, even if only |
---|
47 | * reading data. |
---|
48 | * |
---|
49 | * This implementation will invalidate connections that it detects have had |
---|
50 | * communication errors when processing a request. Problematic hosts will be |
---|
51 | * added to a global blacklist for loadBalanceBlacklistTimeout ms, after which |
---|
52 | * they will be removed from the blacklist and made eligible once again to be |
---|
53 | * selected for new connections. |
---|
54 | * |
---|
55 | * This implementation is thread-safe, but it's questionable whether sharing a |
---|
56 | * connection instance amongst threads is a good idea, given that transactions |
---|
57 | * are scoped to connections in JDBC. |
---|
58 | * |
---|
59 | * @version $Id: $ |
---|
60 | * |
---|
61 | */ |
---|
62 | public class LoadBalancingConnectionProxy implements InvocationHandler, |
---|
63 | PingTarget { |
---|
64 | |
---|
65 | private static Method getLocalTimeMethod; |
---|
66 | |
---|
67 | private long totalPhysicalConnections = 0; |
---|
68 | private long activePhysicalConnections = 0; |
---|
69 | private String hostToRemove = null; |
---|
70 | private long lastUsed = 0; |
---|
71 | private long transactionCount = 0; |
---|
72 | private ConnectionGroup connectionGroup = null; |
---|
73 | private String closedReason = null; |
---|
74 | |
---|
75 | public static final String BLACKLIST_TIMEOUT_PROPERTY_KEY = "loadBalanceBlacklistTimeout"; |
---|
76 | |
---|
77 | static { |
---|
78 | try { |
---|
79 | getLocalTimeMethod = System.class.getMethod("nanoTime", |
---|
80 | new Class[0]); |
---|
81 | } catch (SecurityException e) { |
---|
82 | // ignore |
---|
83 | } catch (NoSuchMethodException e) { |
---|
84 | // ignore |
---|
85 | } |
---|
86 | } |
---|
87 | |
---|
88 | // Lifted from C/J 5.1's JDBC-2.0 connection pool classes, let's merge this |
---|
89 | // if/when this gets into 5.1 |
---|
90 | protected class ConnectionErrorFiringInvocationHandler implements |
---|
91 | InvocationHandler { |
---|
92 | Object invokeOn = null; |
---|
93 | |
---|
94 | public ConnectionErrorFiringInvocationHandler(Object toInvokeOn) { |
---|
95 | invokeOn = toInvokeOn; |
---|
96 | } |
---|
97 | |
---|
98 | public Object invoke(Object proxy, Method method, Object[] args) |
---|
99 | throws Throwable { |
---|
100 | Object result = null; |
---|
101 | |
---|
102 | try { |
---|
103 | result = method.invoke(invokeOn, args); |
---|
104 | |
---|
105 | if (result != null) { |
---|
106 | result = proxyIfInterfaceIsJdbc(result, result.getClass()); |
---|
107 | } |
---|
108 | } catch (InvocationTargetException e) { |
---|
109 | dealWithInvocationException(e); |
---|
110 | } |
---|
111 | |
---|
112 | return result; |
---|
113 | } |
---|
114 | } |
---|
115 | |
---|
116 | protected MySQLConnection currentConn; |
---|
117 | |
---|
118 | protected List<String> hostList; |
---|
119 | |
---|
120 | protected Map<String, ConnectionImpl> liveConnections; |
---|
121 | |
---|
122 | private Map<ConnectionImpl, String> connectionsToHostsMap; |
---|
123 | |
---|
124 | private long[] responseTimes; |
---|
125 | |
---|
126 | private Map<String, Integer> hostsToListIndexMap; |
---|
127 | |
---|
128 | private boolean inTransaction = false; |
---|
129 | |
---|
130 | private long transactionStartTime = 0; |
---|
131 | |
---|
132 | private Properties localProps; |
---|
133 | |
---|
134 | private boolean isClosed = false; |
---|
135 | |
---|
136 | private BalanceStrategy balancer; |
---|
137 | |
---|
138 | private int retriesAllDown; |
---|
139 | |
---|
140 | private static Map<String, Long> globalBlacklist = new HashMap<String, Long>(); |
---|
141 | |
---|
142 | private int globalBlacklistTimeout = 0; |
---|
143 | |
---|
144 | private long connectionGroupProxyID = 0; |
---|
145 | |
---|
146 | private LoadBalanceExceptionChecker exceptionChecker; |
---|
147 | |
---|
148 | private Map<Class<?>, Boolean> jdbcInterfacesForProxyCache = new HashMap<Class<?>, Boolean>(); |
---|
149 | |
---|
150 | private MySQLConnection thisAsConnection = null; |
---|
151 | |
---|
152 | private int autoCommitSwapThreshold = 0; |
---|
153 | |
---|
154 | private static Constructor<?> JDBC_4_LB_CONNECTION_CTOR; |
---|
155 | |
---|
156 | static { |
---|
157 | if(Util.isJdbc4()){ |
---|
158 | try { |
---|
159 | JDBC_4_LB_CONNECTION_CTOR = Class.forName( |
---|
160 | "com.mysql.jdbc.JDBC4LoadBalancedMySQLConnection").getConstructor( |
---|
161 | new Class[] { LoadBalancingConnectionProxy.class}); |
---|
162 | } catch (SecurityException e) { |
---|
163 | throw new RuntimeException(e); |
---|
164 | } catch (NoSuchMethodException e) { |
---|
165 | throw new RuntimeException(e); |
---|
166 | } catch (ClassNotFoundException e) { |
---|
167 | throw new RuntimeException(e); |
---|
168 | } |
---|
169 | } |
---|
170 | } |
---|
171 | |
---|
172 | |
---|
173 | |
---|
174 | |
---|
175 | |
---|
176 | |
---|
177 | |
---|
178 | /** |
---|
179 | * Creates a proxy for java.sql.Connection that routes requests between the |
---|
180 | * given list of host:port and uses the given properties when creating |
---|
181 | * connections. |
---|
182 | * |
---|
183 | * @param hosts |
---|
184 | * @param props |
---|
185 | * @throws SQLException |
---|
186 | */ |
---|
187 | LoadBalancingConnectionProxy(List<String> hosts, Properties props) |
---|
188 | throws SQLException { |
---|
189 | String group = props.getProperty("loadBalanceConnectionGroup", |
---|
190 | null); |
---|
191 | boolean enableJMX = false; |
---|
192 | String enableJMXAsString = props.getProperty("loadBalanceEnableJMX", |
---|
193 | "false"); |
---|
194 | try{ |
---|
195 | enableJMX = Boolean.parseBoolean(enableJMXAsString); |
---|
196 | } catch (Exception e){ |
---|
197 | throw SQLError.createSQLException(Messages.getString( |
---|
198 | "LoadBalancingConnectionProxy.badValueForLoadBalanceEnableJMX", |
---|
199 | new Object[] { enableJMXAsString }), |
---|
200 | SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null); |
---|
201 | } |
---|
202 | |
---|
203 | if(group != null){ |
---|
204 | this.connectionGroup = ConnectionGroupManager.getConnectionGroupInstance(group); |
---|
205 | if(enableJMX){ |
---|
206 | ConnectionGroupManager.registerJmx(); |
---|
207 | } |
---|
208 | this.connectionGroupProxyID = this.connectionGroup.registerConnectionProxy(this, hosts); |
---|
209 | hosts = new ArrayList<String>(this.connectionGroup.getInitialHosts()); |
---|
210 | } |
---|
211 | |
---|
212 | this.hostList = hosts; |
---|
213 | |
---|
214 | int numHosts = this.hostList.size(); |
---|
215 | |
---|
216 | this.liveConnections = new HashMap<String, ConnectionImpl>(numHosts); |
---|
217 | this.connectionsToHostsMap = new HashMap<ConnectionImpl, String>(numHosts); |
---|
218 | this.responseTimes = new long[numHosts]; |
---|
219 | this.hostsToListIndexMap = new HashMap<String, Integer>(numHosts); |
---|
220 | |
---|
221 | this.localProps = (Properties) props.clone(); |
---|
222 | this.localProps.remove(NonRegisteringDriver.HOST_PROPERTY_KEY); |
---|
223 | this.localProps.remove(NonRegisteringDriver.PORT_PROPERTY_KEY); |
---|
224 | |
---|
225 | for (int i = 0; i < numHosts; i++) { |
---|
226 | this.hostsToListIndexMap.put(this.hostList.get(i), Integer.valueOf(i)); |
---|
227 | this.localProps.remove(NonRegisteringDriver.HOST_PROPERTY_KEY + "." |
---|
228 | + (i + 1)); |
---|
229 | this.localProps.remove(NonRegisteringDriver.PORT_PROPERTY_KEY + "." |
---|
230 | + (i + 1)); |
---|
231 | } |
---|
232 | |
---|
233 | this.localProps.remove(NonRegisteringDriver.NUM_HOSTS_PROPERTY_KEY); |
---|
234 | this.localProps.setProperty("useLocalSessionState", "true"); |
---|
235 | |
---|
236 | String strategy = this.localProps.getProperty("loadBalanceStrategy", |
---|
237 | "random"); |
---|
238 | |
---|
239 | String lbExceptionChecker = this.localProps.getProperty("loadBalanceExceptionChecker", |
---|
240 | "com.mysql.jdbc.StandardLoadBalanceExceptionChecker"); |
---|
241 | |
---|
242 | |
---|
243 | String retriesAllDownAsString = this.localProps.getProperty( |
---|
244 | "retriesAllDown", "120"); |
---|
245 | |
---|
246 | try { |
---|
247 | this.retriesAllDown = Integer.parseInt(retriesAllDownAsString); |
---|
248 | } catch (NumberFormatException nfe) { |
---|
249 | throw SQLError.createSQLException(Messages.getString( |
---|
250 | "LoadBalancingConnectionProxy.badValueForRetriesAllDown", |
---|
251 | new Object[] { retriesAllDownAsString }), |
---|
252 | SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null); |
---|
253 | } |
---|
254 | String blacklistTimeoutAsString = this.localProps.getProperty( |
---|
255 | BLACKLIST_TIMEOUT_PROPERTY_KEY, "0"); |
---|
256 | |
---|
257 | try { |
---|
258 | this.globalBlacklistTimeout = Integer |
---|
259 | .parseInt(blacklistTimeoutAsString); |
---|
260 | } catch (NumberFormatException nfe) { |
---|
261 | throw SQLError |
---|
262 | .createSQLException( |
---|
263 | Messages |
---|
264 | .getString( |
---|
265 | "LoadBalancingConnectionProxy.badValueForLoadBalanceBlacklistTimeout", |
---|
266 | new Object[] { retriesAllDownAsString }), |
---|
267 | SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null); |
---|
268 | } |
---|
269 | |
---|
270 | if ("random".equals(strategy)) { |
---|
271 | this.balancer = (BalanceStrategy) Util.loadExtensions(null, props, |
---|
272 | "com.mysql.jdbc.RandomBalanceStrategy", |
---|
273 | "InvalidLoadBalanceStrategy", null).get(0); |
---|
274 | } else if ("bestResponseTime".equals(strategy)) { |
---|
275 | this.balancer = (BalanceStrategy) Util.loadExtensions(null, props, |
---|
276 | "com.mysql.jdbc.BestResponseTimeBalanceStrategy", |
---|
277 | "InvalidLoadBalanceStrategy", null).get(0); |
---|
278 | } else { |
---|
279 | this.balancer = (BalanceStrategy) Util.loadExtensions(null, props, |
---|
280 | strategy, "InvalidLoadBalanceStrategy", null).get(0); |
---|
281 | } |
---|
282 | |
---|
283 | String autoCommitSwapThresholdAsString = props.getProperty("loadBalanceAutoCommitStatementThreshold", |
---|
284 | "0"); |
---|
285 | try { |
---|
286 | this.autoCommitSwapThreshold = Integer.parseInt(autoCommitSwapThresholdAsString); |
---|
287 | } catch (NumberFormatException nfe) { |
---|
288 | throw SQLError.createSQLException(Messages.getString( |
---|
289 | "LoadBalancingConnectionProxy.badValueForLoadBalanceAutoCommitStatementThreshold", |
---|
290 | new Object[] { autoCommitSwapThresholdAsString }), |
---|
291 | SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null); |
---|
292 | } |
---|
293 | |
---|
294 | String autoCommitSwapRegex = props.getProperty("loadBalanceAutoCommitStatementRegex",""); |
---|
295 | if(!("".equals(autoCommitSwapRegex))){ |
---|
296 | try{ |
---|
297 | "".matches(autoCommitSwapRegex); |
---|
298 | } catch (Exception e){ |
---|
299 | throw SQLError.createSQLException(Messages.getString( |
---|
300 | "LoadBalancingConnectionProxy.badValueForLoadBalanceAutoCommitStatementRegex", |
---|
301 | new Object[] { autoCommitSwapRegex }), |
---|
302 | SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null); |
---|
303 | } |
---|
304 | } |
---|
305 | |
---|
306 | if(this.autoCommitSwapThreshold > 0){ |
---|
307 | String statementInterceptors = this.localProps.getProperty("statementInterceptors"); |
---|
308 | if(statementInterceptors == null){ |
---|
309 | this.localProps.setProperty("statementInterceptors", "com.mysql.jdbc.LoadBalancedAutoCommitInterceptor"); |
---|
310 | } else if(statementInterceptors.length() > 0){ |
---|
311 | this.localProps.setProperty("statementInterceptors", statementInterceptors + ",com.mysql.jdbc.LoadBalancedAutoCommitInterceptor"); |
---|
312 | } |
---|
313 | props.setProperty("statementInterceptors", this.localProps.getProperty("statementInterceptors")); |
---|
314 | |
---|
315 | } |
---|
316 | this.balancer.init(null, props); |
---|
317 | |
---|
318 | |
---|
319 | this.exceptionChecker = (LoadBalanceExceptionChecker) Util.loadExtensions(null, props, |
---|
320 | lbExceptionChecker, "InvalidLoadBalanceExceptionChecker", null).get(0); |
---|
321 | this.exceptionChecker.init(null, props); |
---|
322 | |
---|
323 | if(Util.isJdbc4() || JDBC_4_LB_CONNECTION_CTOR != null){ |
---|
324 | thisAsConnection = (MySQLConnection) Util.handleNewInstance(JDBC_4_LB_CONNECTION_CTOR, |
---|
325 | new Object[] {this}, null); |
---|
326 | }else{ |
---|
327 | thisAsConnection = new LoadBalancedMySQLConnection(this); |
---|
328 | } |
---|
329 | pickNewConnection(); |
---|
330 | |
---|
331 | |
---|
332 | } |
---|
333 | |
---|
334 | /** |
---|
335 | * Creates a new physical connection for the given host:port and updates |
---|
336 | * required internal mappings and statistics for that connection. |
---|
337 | * |
---|
338 | * @param hostPortSpec |
---|
339 | * @return |
---|
340 | * @throws SQLException |
---|
341 | */ |
---|
342 | public synchronized ConnectionImpl createConnectionForHost( |
---|
343 | String hostPortSpec) throws SQLException { |
---|
344 | Properties connProps = (Properties) this.localProps.clone(); |
---|
345 | |
---|
346 | String[] hostPortPair = NonRegisteringDriver |
---|
347 | .parseHostPortPair(hostPortSpec); |
---|
348 | String hostName = hostPortPair[NonRegisteringDriver.HOST_NAME_INDEX]; |
---|
349 | String portNumber = hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX]; |
---|
350 | String dbName = connProps |
---|
351 | .getProperty(NonRegisteringDriver.DBNAME_PROPERTY_KEY); |
---|
352 | |
---|
353 | if (hostName == null) { |
---|
354 | throw new SQLException( |
---|
355 | "Could not find a hostname to start a connection to"); |
---|
356 | } |
---|
357 | if (portNumber == null) { |
---|
358 | portNumber = "3306";// use default |
---|
359 | } |
---|
360 | |
---|
361 | connProps.setProperty(NonRegisteringDriver.HOST_PROPERTY_KEY, hostName); |
---|
362 | connProps.setProperty(NonRegisteringDriver.PORT_PROPERTY_KEY, |
---|
363 | portNumber); |
---|
364 | connProps.setProperty(NonRegisteringDriver.HOST_PROPERTY_KEY + ".1", |
---|
365 | hostName); |
---|
366 | connProps.setProperty(NonRegisteringDriver.PORT_PROPERTY_KEY + ".1", |
---|
367 | portNumber); |
---|
368 | connProps.setProperty(NonRegisteringDriver.NUM_HOSTS_PROPERTY_KEY, "1"); |
---|
369 | connProps.setProperty("roundRobinLoadBalance", "false"); // make sure we |
---|
370 | // don't |
---|
371 | // pickup |
---|
372 | // the |
---|
373 | // default |
---|
374 | // value |
---|
375 | |
---|
376 | ConnectionImpl conn = (ConnectionImpl) ConnectionImpl.getInstance( |
---|
377 | hostName, Integer.parseInt(portNumber), connProps, dbName, |
---|
378 | "jdbc:mysql://" + hostName + ":" + portNumber + "/"); |
---|
379 | |
---|
380 | |
---|
381 | |
---|
382 | this.liveConnections.put(hostPortSpec, conn); |
---|
383 | this.connectionsToHostsMap.put(conn, hostPortSpec); |
---|
384 | |
---|
385 | |
---|
386 | this.activePhysicalConnections++; |
---|
387 | this.totalPhysicalConnections++; |
---|
388 | |
---|
389 | conn.setProxy(this.thisAsConnection); |
---|
390 | |
---|
391 | return conn; |
---|
392 | } |
---|
393 | |
---|
394 | /** |
---|
395 | * @param e |
---|
396 | * @throws SQLException |
---|
397 | * @throws Throwable |
---|
398 | * @throws InvocationTargetException |
---|
399 | */ |
---|
400 | void dealWithInvocationException(InvocationTargetException e) |
---|
401 | throws SQLException, Throwable, InvocationTargetException { |
---|
402 | Throwable t = e.getTargetException(); |
---|
403 | |
---|
404 | if (t != null) { |
---|
405 | if (t instanceof SQLException && shouldExceptionTriggerFailover((SQLException) t )) { |
---|
406 | invalidateCurrentConnection(); |
---|
407 | pickNewConnection(); |
---|
408 | } |
---|
409 | |
---|
410 | throw t; |
---|
411 | } |
---|
412 | |
---|
413 | throw e; |
---|
414 | } |
---|
415 | |
---|
416 | /** |
---|
417 | * Closes current connection and removes it from required mappings. |
---|
418 | * |
---|
419 | * @throws SQLException |
---|
420 | */ |
---|
421 | synchronized void invalidateCurrentConnection() throws SQLException { |
---|
422 | try { |
---|
423 | if (!this.currentConn.isClosed()) { |
---|
424 | this.currentConn.close(); |
---|
425 | } |
---|
426 | |
---|
427 | } finally { |
---|
428 | // add host to the global blacklist, if enabled |
---|
429 | if (this.isGlobalBlacklistEnabled()) { |
---|
430 | this.addToGlobalBlacklist(this.connectionsToHostsMap |
---|
431 | .get(this.currentConn)); |
---|
432 | |
---|
433 | } |
---|
434 | // remove from liveConnections |
---|
435 | this.liveConnections.remove(this.connectionsToHostsMap |
---|
436 | .get(this.currentConn)); |
---|
437 | Object mappedHost = this.connectionsToHostsMap |
---|
438 | .remove(this.currentConn); |
---|
439 | if (mappedHost != null |
---|
440 | && this.hostsToListIndexMap.containsKey(mappedHost)) { |
---|
441 | int hostIndex = (this.hostsToListIndexMap |
---|
442 | .get(mappedHost)).intValue(); |
---|
443 | // reset the statistics for the host |
---|
444 | synchronized (this.responseTimes) { |
---|
445 | this.responseTimes[hostIndex] = 0; |
---|
446 | } |
---|
447 | } |
---|
448 | } |
---|
449 | } |
---|
450 | |
---|
451 | private void closeAllConnections() { |
---|
452 | synchronized (this) { |
---|
453 | // close all underlying connections |
---|
454 | Iterator<ConnectionImpl> allConnections = this.liveConnections.values().iterator(); |
---|
455 | |
---|
456 | while (allConnections.hasNext()) { |
---|
457 | try { |
---|
458 | this.activePhysicalConnections--; |
---|
459 | allConnections.next().close(); |
---|
460 | } catch (SQLException e) { |
---|
461 | } |
---|
462 | } |
---|
463 | |
---|
464 | if (!this.isClosed) { |
---|
465 | this.balancer.destroy(); |
---|
466 | if(this.connectionGroup != null){ |
---|
467 | this.connectionGroup.closeConnectionProxy(this); |
---|
468 | } |
---|
469 | } |
---|
470 | |
---|
471 | this.liveConnections.clear(); |
---|
472 | this.connectionsToHostsMap.clear(); |
---|
473 | } |
---|
474 | |
---|
475 | } |
---|
476 | |
---|
477 | |
---|
478 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
---|
479 | return this.invoke(proxy, method, args, true); |
---|
480 | } |
---|
481 | |
---|
482 | /** |
---|
483 | * Proxies method invocation on the java.sql.Connection interface, trapping |
---|
484 | * "close", "isClosed" and "commit/rollback" (to switch connections for load |
---|
485 | * balancing). |
---|
486 | * |
---|
487 | * @param proxy |
---|
488 | * @param method |
---|
489 | * @param args |
---|
490 | * @param swapAtTransactionBoundary |
---|
491 | * @return |
---|
492 | * @throws Throwable |
---|
493 | */ |
---|
494 | public synchronized Object invoke(Object proxy, Method method, Object[] args, boolean swapAtTransactionBoundary) |
---|
495 | throws Throwable { |
---|
496 | |
---|
497 | String methodName = method.getName(); |
---|
498 | |
---|
499 | if("getLoadBalanceSafeProxy".equals(methodName)){ |
---|
500 | return this.currentConn; |
---|
501 | } |
---|
502 | |
---|
503 | if ("equals".equals(methodName) && args.length == 1) { |
---|
504 | if (args[0] instanceof Proxy) { |
---|
505 | return Boolean.valueOf((((Proxy) args[0]).equals(this))); |
---|
506 | } |
---|
507 | return Boolean.valueOf(this.equals(args[0])); |
---|
508 | } |
---|
509 | |
---|
510 | if ("hashCode".equals(methodName)) { |
---|
511 | return Integer.valueOf(this.hashCode()); |
---|
512 | } |
---|
513 | |
---|
514 | if ("close".equals(methodName)) { |
---|
515 | closeAllConnections(); |
---|
516 | |
---|
517 | this.isClosed = true; |
---|
518 | this.closedReason = "Connection explicitly closed."; |
---|
519 | |
---|
520 | return null; |
---|
521 | } |
---|
522 | |
---|
523 | if ("isClosed".equals(methodName)) { |
---|
524 | return Boolean.valueOf(this.isClosed); |
---|
525 | } |
---|
526 | |
---|
527 | if (this.isClosed) { |
---|
528 | String reason = "No operations allowed after connection closed."; |
---|
529 | if(this.closedReason != null){ |
---|
530 | reason += (" " + this.closedReason); |
---|
531 | } |
---|
532 | throw SQLError.createSQLException( |
---|
533 | reason, |
---|
534 | SQLError.SQL_STATE_CONNECTION_NOT_OPEN, null /* |
---|
535 | * no access to |
---|
536 | * a interceptor |
---|
537 | * here... |
---|
538 | */); |
---|
539 | } |
---|
540 | |
---|
541 | if (!inTransaction) { |
---|
542 | this.inTransaction = true; |
---|
543 | this.transactionStartTime = getLocalTimeBestResolution(); |
---|
544 | this.transactionCount++; |
---|
545 | } |
---|
546 | |
---|
547 | Object result = null; |
---|
548 | |
---|
549 | try { |
---|
550 | this.lastUsed = System.currentTimeMillis(); |
---|
551 | result = method.invoke(thisAsConnection, args); |
---|
552 | |
---|
553 | if (result != null) { |
---|
554 | if (result instanceof com.mysql.jdbc.Statement) { |
---|
555 | ((com.mysql.jdbc.Statement) result).setPingTarget(this); |
---|
556 | } |
---|
557 | |
---|
558 | result = proxyIfInterfaceIsJdbc(result, result.getClass()); |
---|
559 | } |
---|
560 | } catch (InvocationTargetException e) { |
---|
561 | dealWithInvocationException(e); |
---|
562 | } finally { |
---|
563 | if (swapAtTransactionBoundary && ("commit".equals(methodName) || "rollback".equals(methodName))) { |
---|
564 | this.inTransaction = false; |
---|
565 | |
---|
566 | // Update stats |
---|
567 | String host = this.connectionsToHostsMap.get(this.currentConn); |
---|
568 | // avoid NPE if the connection has already been removed from |
---|
569 | // connectionsToHostsMap |
---|
570 | // in invalidateCurrenctConnection() |
---|
571 | if (host != null) { |
---|
572 | synchronized (this.responseTimes) { |
---|
573 | int hostIndex = (this.hostsToListIndexMap |
---|
574 | .get(host)).intValue(); |
---|
575 | |
---|
576 | if(hostIndex < this.responseTimes.length){ |
---|
577 | this.responseTimes[hostIndex] = getLocalTimeBestResolution() |
---|
578 | - this.transactionStartTime; |
---|
579 | } |
---|
580 | } |
---|
581 | } |
---|
582 | pickNewConnection(); |
---|
583 | } |
---|
584 | } |
---|
585 | |
---|
586 | return result; |
---|
587 | } |
---|
588 | |
---|
589 | /** |
---|
590 | * Picks the "best" connection to use for the next transaction based on the |
---|
591 | * BalanceStrategy in use. |
---|
592 | * |
---|
593 | * @throws SQLException |
---|
594 | */ |
---|
595 | protected synchronized void pickNewConnection() throws SQLException { |
---|
596 | if (this.currentConn == null) { // startup |
---|
597 | this.currentConn = this.balancer.pickConnection(this, Collections |
---|
598 | .unmodifiableList(this.hostList), Collections |
---|
599 | .unmodifiableMap(this.liveConnections), |
---|
600 | this.responseTimes.clone(), this.retriesAllDown); |
---|
601 | return; |
---|
602 | } |
---|
603 | |
---|
604 | if(this.currentConn.isClosed()){ |
---|
605 | invalidateCurrentConnection(); |
---|
606 | } |
---|
607 | |
---|
608 | int pingTimeout = this.currentConn.getLoadBalancePingTimeout(); |
---|
609 | boolean pingBeforeReturn = this.currentConn.getLoadBalanceValidateConnectionOnSwapServer(); |
---|
610 | |
---|
611 | for(int hostsTried = 0, hostsToTry = this.hostList.size(); hostsTried <= hostsToTry; hostsTried++){ |
---|
612 | try{ |
---|
613 | ConnectionImpl newConn = this.balancer.pickConnection( |
---|
614 | |
---|
615 | this, Collections.unmodifiableList(this.hostList), Collections |
---|
616 | .unmodifiableMap(this.liveConnections), |
---|
617 | this.responseTimes.clone(), this.retriesAllDown); |
---|
618 | |
---|
619 | if (this.currentConn != null) { |
---|
620 | if(pingBeforeReturn){ |
---|
621 | if(pingTimeout == 0){ |
---|
622 | newConn.ping(); |
---|
623 | } else { |
---|
624 | newConn.pingInternal(true, pingTimeout); |
---|
625 | } |
---|
626 | } |
---|
627 | |
---|
628 | syncSessionState(this.currentConn, newConn); |
---|
629 | } |
---|
630 | |
---|
631 | this.currentConn = newConn; |
---|
632 | return; |
---|
633 | } catch (SQLException e){ |
---|
634 | |
---|
635 | if (shouldExceptionTriggerFailover(e)) { |
---|
636 | // connection error, close up shop on current |
---|
637 | // connection |
---|
638 | invalidateCurrentConnection(); |
---|
639 | } |
---|
640 | } |
---|
641 | |
---|
642 | } |
---|
643 | // no hosts available to swap connection to, close up. |
---|
644 | this.isClosed = true; |
---|
645 | this.closedReason = "Connection closed after inability to pick valid new connection during fail-over."; |
---|
646 | |
---|
647 | } |
---|
648 | |
---|
649 | /** |
---|
650 | * Recursively checks for interfaces on the given object to determine if it |
---|
651 | * implements a java.sql interface, and if so, proxies the instance so that |
---|
652 | * we can catch and fire SQL errors. |
---|
653 | * |
---|
654 | * @param toProxy |
---|
655 | * @param clazz |
---|
656 | * @return |
---|
657 | */ |
---|
658 | Object proxyIfInterfaceIsJdbc(Object toProxy, Class<?> clazz) { |
---|
659 | |
---|
660 | if(isInterfaceJdbc(clazz)){ |
---|
661 | |
---|
662 | Class<?>[] interfacesToProxy = getAllInterfacesToProxy(clazz); |
---|
663 | |
---|
664 | return Proxy.newProxyInstance(toProxy.getClass() |
---|
665 | .getClassLoader(), interfacesToProxy, |
---|
666 | createConnectionProxy(toProxy)); |
---|
667 | } |
---|
668 | |
---|
669 | return toProxy; |
---|
670 | } |
---|
671 | |
---|
672 | private Map<Class<?>, Class<?>[]> allInterfacesToProxy = new HashMap<Class<?>, Class<?>[]>(); |
---|
673 | |
---|
674 | private Class<?>[] getAllInterfacesToProxy(Class<?> clazz) { |
---|
675 | Class<?>[] interfacesToProxy = this.allInterfacesToProxy.get(clazz); |
---|
676 | |
---|
677 | if (interfacesToProxy != null) { |
---|
678 | return interfacesToProxy; |
---|
679 | } |
---|
680 | |
---|
681 | List<Class<?>> interfaces = new LinkedList<Class<?>>(); |
---|
682 | |
---|
683 | Class<?> superClass = clazz; |
---|
684 | |
---|
685 | while (!(superClass.equals(Object.class))) { |
---|
686 | Class<?>[] declared = superClass.getInterfaces(); |
---|
687 | |
---|
688 | for (int i = 0; i < declared.length; i++) { |
---|
689 | interfaces.add(declared[i]); |
---|
690 | } |
---|
691 | |
---|
692 | superClass = superClass.getSuperclass(); |
---|
693 | } |
---|
694 | |
---|
695 | interfacesToProxy = new Class[interfaces.size()]; |
---|
696 | interfaces.toArray(interfacesToProxy); |
---|
697 | |
---|
698 | this.allInterfacesToProxy.put(clazz, interfacesToProxy); |
---|
699 | |
---|
700 | return interfacesToProxy; |
---|
701 | } |
---|
702 | |
---|
703 | |
---|
704 | private boolean isInterfaceJdbc(Class<?> clazz){ |
---|
705 | if(this.jdbcInterfacesForProxyCache.containsKey(clazz)){ |
---|
706 | return (this.jdbcInterfacesForProxyCache.get(clazz)).booleanValue(); |
---|
707 | } |
---|
708 | |
---|
709 | Class<?>[] interfaces = clazz.getInterfaces(); |
---|
710 | |
---|
711 | for (int i = 0; i < interfaces.length; i++) { |
---|
712 | String packageName = interfaces[i].getPackage().getName(); |
---|
713 | |
---|
714 | if ("java.sql".equals(packageName) |
---|
715 | || "javax.sql".equals(packageName) |
---|
716 | || "com.mysql.jdbc".equals(packageName)) { |
---|
717 | this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(true)); |
---|
718 | |
---|
719 | return true; |
---|
720 | } |
---|
721 | |
---|
722 | if(isInterfaceJdbc(interfaces[i])){ |
---|
723 | this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(true)); |
---|
724 | |
---|
725 | return true; |
---|
726 | } |
---|
727 | } |
---|
728 | |
---|
729 | this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(false)); |
---|
730 | return false; |
---|
731 | |
---|
732 | } |
---|
733 | |
---|
734 | protected ConnectionErrorFiringInvocationHandler createConnectionProxy( |
---|
735 | Object toProxy) { |
---|
736 | return new ConnectionErrorFiringInvocationHandler(toProxy); |
---|
737 | } |
---|
738 | |
---|
739 | /** |
---|
740 | * Returns best-resolution representation of local time, using nanoTime() if |
---|
741 | * available, otherwise defaulting to currentTimeMillis(). |
---|
742 | */ |
---|
743 | private static long getLocalTimeBestResolution() { |
---|
744 | if (getLocalTimeMethod != null) { |
---|
745 | try { |
---|
746 | return ((Long) getLocalTimeMethod.invoke(null, (Object[])null)) |
---|
747 | .longValue(); |
---|
748 | } catch (IllegalArgumentException e) { |
---|
749 | // ignore - we fall through to currentTimeMillis() |
---|
750 | } catch (IllegalAccessException e) { |
---|
751 | // ignore - we fall through to currentTimeMillis() |
---|
752 | } catch (InvocationTargetException e) { |
---|
753 | // ignore - we fall through to currentTimeMillis() |
---|
754 | } |
---|
755 | } |
---|
756 | |
---|
757 | return System.currentTimeMillis(); |
---|
758 | } |
---|
759 | |
---|
760 | public synchronized void doPing() throws SQLException { |
---|
761 | SQLException se = null; |
---|
762 | boolean foundHost = false; |
---|
763 | int pingTimeout = this.currentConn.getLoadBalancePingTimeout(); |
---|
764 | synchronized (this) { |
---|
765 | for (Iterator<String> i = this.hostList.iterator(); i.hasNext();) { |
---|
766 | String host = i.next(); |
---|
767 | ConnectionImpl conn = this.liveConnections.get(host); |
---|
768 | if (conn == null) { |
---|
769 | continue; |
---|
770 | } |
---|
771 | try { |
---|
772 | if(pingTimeout == 0){ |
---|
773 | conn.ping(); |
---|
774 | } else { |
---|
775 | conn.pingInternal(true, pingTimeout); |
---|
776 | } |
---|
777 | foundHost = true; |
---|
778 | } catch (SQLException e) { |
---|
779 | this.activePhysicalConnections--; |
---|
780 | // give up if it is the current connection, otherwise NPE |
---|
781 | // faking resultset later. |
---|
782 | if (host.equals(this.connectionsToHostsMap |
---|
783 | .get(this.currentConn))) { |
---|
784 | // clean up underlying connections, since connection |
---|
785 | // pool won't do it |
---|
786 | closeAllConnections(); |
---|
787 | this.isClosed = true; |
---|
788 | this.closedReason = "Connection closed because ping of current connection failed."; |
---|
789 | throw e; |
---|
790 | } |
---|
791 | |
---|
792 | // if the Exception is caused by ping connection lifetime |
---|
793 | // checks, don't add to blacklist |
---|
794 | if (e |
---|
795 | .getMessage() |
---|
796 | .equals( |
---|
797 | Messages |
---|
798 | .getString("Connection.exceededConnectionLifetime"))) { |
---|
799 | // only set the return Exception if it's null |
---|
800 | if (se == null) { |
---|
801 | se = e; |
---|
802 | } |
---|
803 | } else { |
---|
804 | // overwrite the return Exception no matter what |
---|
805 | se = e; |
---|
806 | if (this.isGlobalBlacklistEnabled()) { |
---|
807 | this.addToGlobalBlacklist(host); |
---|
808 | } |
---|
809 | } |
---|
810 | // take the connection out of the liveConnections Map |
---|
811 | this.liveConnections.remove(this.connectionsToHostsMap |
---|
812 | .get(conn)); |
---|
813 | } |
---|
814 | } |
---|
815 | } |
---|
816 | // if there were no successful pings |
---|
817 | if (!foundHost) { |
---|
818 | closeAllConnections(); |
---|
819 | this.isClosed = true; |
---|
820 | this.closedReason = "Connection closed due to inability to ping any active connections."; |
---|
821 | // throw the stored Exception, if exists |
---|
822 | if (se != null) { |
---|
823 | throw se; |
---|
824 | } |
---|
825 | // or create a new SQLException and throw it, must be no |
---|
826 | // liveConnections |
---|
827 | ((ConnectionImpl) this.currentConn) |
---|
828 | .throwConnectionClosedException(); |
---|
829 | } |
---|
830 | } |
---|
831 | |
---|
832 | public void addToGlobalBlacklist(String host, long timeout) { |
---|
833 | if (this.isGlobalBlacklistEnabled()) { |
---|
834 | synchronized (globalBlacklist) { |
---|
835 | globalBlacklist.put(host, Long.valueOf(timeout)); |
---|
836 | } |
---|
837 | } |
---|
838 | } |
---|
839 | |
---|
840 | public void addToGlobalBlacklist(String host){ |
---|
841 | addToGlobalBlacklist(host, System.currentTimeMillis() |
---|
842 | + this.globalBlacklistTimeout); |
---|
843 | |
---|
844 | } |
---|
845 | |
---|
846 | public boolean isGlobalBlacklistEnabled() { |
---|
847 | return (this.globalBlacklistTimeout > 0); |
---|
848 | } |
---|
849 | |
---|
850 | public synchronized Map<String, Long> getGlobalBlacklist() { |
---|
851 | if (!this.isGlobalBlacklistEnabled()) { |
---|
852 | String localHostToRemove = this.hostToRemove; |
---|
853 | |
---|
854 | if(hostToRemove != null){ |
---|
855 | HashMap<String, Long> fakedBlacklist = new HashMap<String, Long>(); |
---|
856 | fakedBlacklist.put(localHostToRemove, Long.valueOf(System.currentTimeMillis() + 5000)); |
---|
857 | return fakedBlacklist; |
---|
858 | } |
---|
859 | |
---|
860 | return new HashMap<String, Long>(1); |
---|
861 | } |
---|
862 | |
---|
863 | // Make a local copy of the blacklist |
---|
864 | Map<String, Long> blacklistClone = new HashMap<String, Long>(globalBlacklist.size()); |
---|
865 | // Copy everything from synchronized global blacklist to local copy for |
---|
866 | // manipulation |
---|
867 | synchronized (globalBlacklist) { |
---|
868 | blacklistClone.putAll(globalBlacklist); |
---|
869 | } |
---|
870 | Set<String> keys = blacklistClone.keySet(); |
---|
871 | |
---|
872 | // we're only interested in blacklisted hosts that are in the hostList |
---|
873 | keys.retainAll(this.hostList); |
---|
874 | |
---|
875 | // Don't need to synchronize here as we using a local copy |
---|
876 | for (Iterator<String> i = keys.iterator(); i.hasNext();) { |
---|
877 | String host = i.next(); |
---|
878 | // OK if null is returned because another thread already purged Map |
---|
879 | // entry. |
---|
880 | Long timeout = globalBlacklist.get(host); |
---|
881 | if (timeout != null |
---|
882 | && timeout.longValue() < System.currentTimeMillis()) { |
---|
883 | // Timeout has expired, remove from blacklist |
---|
884 | synchronized (globalBlacklist) { |
---|
885 | globalBlacklist.remove(host); |
---|
886 | } |
---|
887 | i.remove(); |
---|
888 | } |
---|
889 | |
---|
890 | } |
---|
891 | if (keys.size() == this.hostList.size()) { |
---|
892 | // return an empty blacklist, let the BalanceStrategy |
---|
893 | // implementations try to connect to everything |
---|
894 | // since it appears that all hosts are unavailable - we don't want |
---|
895 | // to wait for |
---|
896 | // loadBalanceBlacklistTimeout to expire. |
---|
897 | return new HashMap<String, Long>(1); |
---|
898 | } |
---|
899 | |
---|
900 | return blacklistClone; |
---|
901 | } |
---|
902 | |
---|
903 | public boolean shouldExceptionTriggerFailover(SQLException ex){ |
---|
904 | return this.exceptionChecker.shouldExceptionTriggerFailover(ex); |
---|
905 | |
---|
906 | } |
---|
907 | |
---|
908 | public void removeHostWhenNotInUse(String host) |
---|
909 | throws SQLException { |
---|
910 | int timeBetweenChecks = 1000; |
---|
911 | long timeBeforeHardFail = 15000; |
---|
912 | |
---|
913 | synchronized (this) { |
---|
914 | addToGlobalBlacklist(host, timeBeforeHardFail + 1000); |
---|
915 | |
---|
916 | long cur = System.currentTimeMillis(); |
---|
917 | |
---|
918 | while (System.currentTimeMillis() - timeBeforeHardFail < cur) { |
---|
919 | |
---|
920 | this.hostToRemove = host; |
---|
921 | |
---|
922 | if (!host.equals(this.currentConn.getHost())) { |
---|
923 | removeHost(host); |
---|
924 | return; |
---|
925 | } |
---|
926 | } |
---|
927 | } |
---|
928 | |
---|
929 | try { |
---|
930 | Thread.sleep(timeBetweenChecks); |
---|
931 | } catch (InterruptedException e) { |
---|
932 | // better to swallow this and retry. |
---|
933 | } |
---|
934 | |
---|
935 | removeHost(host); |
---|
936 | } |
---|
937 | |
---|
938 | public synchronized void removeHost(String host) throws SQLException { |
---|
939 | |
---|
940 | if (this.connectionGroup != null) { |
---|
941 | if (this.connectionGroup.getInitialHosts().size() == 1 |
---|
942 | && this.connectionGroup.getInitialHosts().contains(host)) { |
---|
943 | throw SQLError.createSQLException( |
---|
944 | "Cannot remove only configured host.", null); |
---|
945 | } |
---|
946 | |
---|
947 | this.hostToRemove = host; |
---|
948 | |
---|
949 | if (host.equals(this.currentConn.getHost())) { |
---|
950 | closeAllConnections(); |
---|
951 | } else { |
---|
952 | this.connectionsToHostsMap.remove(this.liveConnections |
---|
953 | .remove(host)); |
---|
954 | Integer idx = this.hostsToListIndexMap.remove(host); |
---|
955 | long[] newResponseTimes = new long[this.responseTimes.length - 1]; |
---|
956 | int newIdx = 0; |
---|
957 | for (Iterator<String> i = this.hostList.iterator(); i.hasNext(); newIdx++) { |
---|
958 | String copyHost = i.next(); |
---|
959 | if (idx != null |
---|
960 | && idx.intValue() < this.responseTimes.length) { |
---|
961 | newResponseTimes[newIdx] = this.responseTimes[idx |
---|
962 | .intValue()]; |
---|
963 | this.hostsToListIndexMap.put(copyHost, |
---|
964 | Integer.valueOf(newIdx)); |
---|
965 | } |
---|
966 | } |
---|
967 | this.responseTimes = newResponseTimes; |
---|
968 | } |
---|
969 | } |
---|
970 | |
---|
971 | } |
---|
972 | |
---|
973 | public synchronized boolean addHost(String host) { |
---|
974 | |
---|
975 | if (this.hostsToListIndexMap.containsKey(host)) { |
---|
976 | return false; |
---|
977 | } |
---|
978 | |
---|
979 | long[] newResponseTimes = new long[this.responseTimes.length + 1]; |
---|
980 | |
---|
981 | for (int i = 0; i < this.responseTimes.length; i++) { |
---|
982 | newResponseTimes[i] = this.responseTimes[i]; |
---|
983 | } |
---|
984 | |
---|
985 | this.responseTimes = newResponseTimes; |
---|
986 | this.hostList.add(host); |
---|
987 | this.hostsToListIndexMap.put(host, |
---|
988 | Integer.valueOf(this.responseTimes.length - 1)); |
---|
989 | |
---|
990 | return true; |
---|
991 | } |
---|
992 | |
---|
993 | public synchronized long getLastUsed(){ |
---|
994 | return this.lastUsed; |
---|
995 | } |
---|
996 | |
---|
997 | public synchronized boolean inTransaction(){ |
---|
998 | return this.inTransaction; |
---|
999 | } |
---|
1000 | |
---|
1001 | public synchronized long getTransactionCount(){ |
---|
1002 | return this.transactionCount; |
---|
1003 | } |
---|
1004 | |
---|
1005 | public synchronized long getActivePhysicalConnectionCount(){ |
---|
1006 | return this.activePhysicalConnections; |
---|
1007 | } |
---|
1008 | |
---|
1009 | public synchronized long getTotalPhysicalConnectionCount(){ |
---|
1010 | return this.totalPhysicalConnections; |
---|
1011 | } |
---|
1012 | |
---|
1013 | public synchronized long getConnectionGroupProxyID(){ |
---|
1014 | return this.connectionGroupProxyID; |
---|
1015 | } |
---|
1016 | |
---|
1017 | public synchronized String getCurrentActiveHost() { |
---|
1018 | MySQLConnection c = this.currentConn; |
---|
1019 | if(c != null){ |
---|
1020 | Object o = this.connectionsToHostsMap.get(c); |
---|
1021 | if(o != null){ |
---|
1022 | return o.toString(); |
---|
1023 | } |
---|
1024 | } |
---|
1025 | return null; |
---|
1026 | } |
---|
1027 | |
---|
1028 | public synchronized long getCurrentTransactionDuration(){ |
---|
1029 | |
---|
1030 | if (this.inTransaction && this.transactionStartTime > 0) { |
---|
1031 | return getLocalTimeBestResolution() - this.transactionStartTime; |
---|
1032 | } |
---|
1033 | |
---|
1034 | return 0; |
---|
1035 | } |
---|
1036 | |
---|
1037 | protected void syncSessionState(Connection initial, Connection target) |
---|
1038 | throws SQLException { |
---|
1039 | if (initial == null || target == null) { |
---|
1040 | return; |
---|
1041 | } |
---|
1042 | target.setAutoCommit(initial.getAutoCommit()); |
---|
1043 | target.setCatalog(initial.getCatalog()); |
---|
1044 | target.setTransactionIsolation(initial.getTransactionIsolation()); |
---|
1045 | target.setReadOnly(initial.isReadOnly()); |
---|
1046 | |
---|
1047 | } |
---|
1048 | |
---|
1049 | } |
---|