background
When using dubbo, you usually encounter the property timeout, which is used to set a timeout for a service call. If the service does not return a result within the set time, a call timeout exception will be thrown: TimeoutException, during use, we sometimes set a timeout value for both provider and consumer configurations, then the service has been invokedWhich one will prevail in the journey?This paper mainly analyses and extends this problem
Three settings
Take the provider configuration as an example:
- Method Level
The settings are as follows:
<dubbo:service interface="fy.test.service.TestService" ref="testServiceImpl"> <dubbo:method name="test" timeout="10000"/> </dubbo:service>
- Interface Level
<dubbo:service interface="fy.test.service.TestService" ref="testServiceImpl" timeout="10000"/>
- Global Level
<dubbo:provider timeout="10000"/>
Priority Selection
In dubbo, if both provider s and consumer s are configured with the same attribute, such as timeout analyzed in this article, there is actually a priority, priority:
Consumer method configuration > provider method configuration > consumer interface configuration > provider interface configuration > consumer global configuration > provider global configuration.So the questions raised at the beginning of this article have a result, which will be interpreted with the source code, which is simple and handled in the method of converting the list of services into DubboInvlker in the RegistryDirectory class:
private Map<String, Invoker<T>> toInvokers(List<URL> urls) { Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>(); if (urls == null || urls.isEmpty()) { return newUrlInvokerMap; } Set<String> keys = new HashSet<String>(); String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY); for (URL providerUrl : urls) { // If protocol is configured at the reference side, only the matching protocol is selected if (queryProtocols != null && queryProtocols.length() > 0) { boolean accept = false; String[] acceptProtocols = queryProtocols.split(","); for (String acceptProtocol : acceptProtocols) { if (providerUrl.getProtocol().equals(acceptProtocol)) { accept = true; break; } } if (!accept) { continue; } } if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) { continue; } if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) { logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() + " in notified url: " + providerUrl + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost() + ", supported protocol: " + ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions())); continue; } // The main point is this method URL url = mergeUrl(providerUrl); String key = url.toFullString(); // The parameter urls are sorted if (keys.contains(key)) { // Repeated url continue; } keys.add(key); // Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key); if (invoker == null) { // Not in the cache, refer again try { boolean enabled = true; if (url.hasParameter(Constants.DISABLED_KEY)) { enabled = !url.getParameter(Constants.DISABLED_KEY, false); } else { enabled = url.getParameter(Constants.ENABLED_KEY, true); } if (enabled) { invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl); } } catch (Throwable t) { logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t); } if (invoker != null) { // Put new invoker in cache newUrlInvokerMap.put(key, invoker); } } else { newUrlInvokerMap.put(key, invoker); } } keys.clear(); return newUrlInvokerMap; }
The focus is on the mergeUrl() method above, which integrates the url parameters of provider and comsumer, and
The mergeUrl() method calls the ClusterUtils.mergeUrl method for integration because it is simpler, that is, it integrates some parameters and overrides them with the consumer parameter. Let's not analyze it here. If interested students can study it.
timeout handler
If timeout is set in the configuration, then how is it handled in the code? Let's expand on how timeout is handled in dubbo, call service methods, and finally call the DubboInvoker.doInvoke method. Let's start with this method:
@Override protected Result doInvoke(final Invocation invocation) throws Throwable { RpcInvocation inv = (RpcInvocation) invocation; final String methodName = RpcUtils.getMethodName(invocation); inv.setAttachment(Constants.PATH_KEY, getUrl().getPath()); inv.setAttachment(Constants.VERSION_KEY, version); ExchangeClient currentClient; if (clients.length == 1) { currentClient = clients[0]; } else { currentClient = clients[index.getAndIncrement() % clients.length]; } try { boolean isAsync = RpcUtils.isAsync(getUrl(), invocation); boolean isAsyncFuture = RpcUtils.isReturnTypeFuture(inv); boolean isOneway = RpcUtils.isOneway(getUrl(), invocation); int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); if (isOneway) { boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false); currentClient.send(inv, isSent); RpcContext.getContext().setFuture(null); return new RpcResult(); } else if (isAsync) { ResponseFuture future = currentClient.request(inv, timeout); // For compatibility FutureAdapter<Object> futureAdapter = new FutureAdapter<>(future); RpcContext.getContext().setFuture(futureAdapter); Result result; // Asynchronous Processing if (isAsyncFuture) { // register resultCallback, sometimes we need the async result being processed by the filter chain. result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false); } else { result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false); } return result; } else { // Synchronization RpcContext.getContext().setFuture(null); return (Result) currentClient.request(inv, timeout).get(); } } catch (TimeoutException e) { throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); } catch (RemotingException e) { throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); } }
In this method, let's analyze in synchronous mode. Look at the request method, which returns a DefaultFuture class and calls the DefaultFuture.get() method. This actually involves a technique to synchronize in asynchronous mode. Let's not analyze it here, so the focus is on the get() method:
@Override public Object get() throws RemotingException { return get(timeout); } @Override public Object get(int timeout) throws RemotingException { if (timeout <= 0) { timeout = Constants.DEFAULT_TIMEOUT; } if (!isDone()) { long start = System.currentTimeMillis(); lock.lock(); try { while (!isDone()) { done.await(timeout, TimeUnit.MILLISECONDS); if (isDone() || System.currentTimeMillis() - start > timeout) { break; } } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } if (!isDone()) { throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); } } return returnFromResponse(); }
When the get() method is called, the get(timeout) method is called. In this method, a timeout field is passed, and timeout is the parameter we configure. In this method, let's focus on the following code block:
if (!isDone()) { long start = System.currentTimeMillis(); lock.lock(); try { while (!isDone()) { // Thread Blocking done.await(timeout, TimeUnit.MILLISECONDS); if (isDone() || System.currentTimeMillis() - start > timeout) { break; } } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } // In the timeout period, if there is no result, a timeout exception is thrown if (!isDone()) { throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); } }
Focus on the await() method, which blocks timeout time. If the blocking time is up, it wakes up execution down, times out and jumps out of the while loop to determine if there is a result returned, and if not, throws a timeout exception.At this point, the principle of timeout is almost the same. There is one more thing to note in this class, DefaultFuture, that when you initialize the DefaultFuture object, you create a timeout delay task, which is the timeout value, in which the signal() method is also called to wake up the blocking