/* * Copyright 2014 Avanza Bank AB * * 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 * * http://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 com.avanza.astrix.remoting.server; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.avanza.astrix.beans.config.AstrixConfig; import com.avanza.astrix.beans.core.AstrixSettings; import com.avanza.astrix.config.DynamicBooleanProperty; import com.avanza.astrix.context.mbeans.MBeanExporter; import com.avanza.astrix.context.metrics.Metrics; import com.avanza.astrix.context.metrics.Timer; import com.avanza.astrix.core.ServiceInvocationException; import com.avanza.astrix.core.function.Command; import com.avanza.astrix.core.util.ReflectionUtil; import com.avanza.astrix.modules.AstrixInject; import com.avanza.astrix.remoting.client.AstrixServiceInvocationRequest; import com.avanza.astrix.remoting.client.AstrixServiceInvocationResponse; import com.avanza.astrix.remoting.client.AstrixServiceInvocationResponseHeaders; import com.avanza.astrix.remoting.client.MissingServiceMethodException; import com.avanza.astrix.versioning.core.AstrixObjectSerializer; /** * Server side component used to invoke exported services. <p> * * @author Elias Lindholm (elilin) * */ class AstrixServiceActivatorImpl implements AstrixServiceActivator { private static final Logger logger = LoggerFactory.getLogger(AstrixServiceActivatorImpl.class); private final ConcurrentMap<String, PublishedService<?>> serviceByType = new ConcurrentHashMap<>(); private final Metrics metrics; private final MBeanExporter mbeanExporter; private final ServiceInvocationMonitor allServicesAggregated; private final DynamicBooleanProperty exportedServiceMetricsEnabled; @AstrixInject public AstrixServiceActivatorImpl(AstrixConfig astrixConfig, Metrics metrics, MBeanExporter mbeanExporter) { this(astrixConfig.get(AstrixSettings.EXPORTED_SERVICE_METRICS_ENABLED), metrics, mbeanExporter); } // For testnig AstrixServiceActivatorImpl(DynamicBooleanProperty exportedServiceMetricsEnabled, Metrics metrics, MBeanExporter mbeanExporter) { this.exportedServiceMetricsEnabled = exportedServiceMetricsEnabled; this.metrics = metrics; this.mbeanExporter = mbeanExporter; // Monitor for aggregated stats for all exported services this.allServicesAggregated = new ServiceInvocationMonitor(metrics.createTimer()); mbeanExporter.registerMBean(this.allServicesAggregated, "ExportedServices", "AllServicesAggregated"); } private static class ServiceInvocationMonitors { private final List<ServiceInvocationMonitor> monitor; private final DynamicBooleanProperty serviceMonitorEnabled; public ServiceInvocationMonitors(DynamicBooleanProperty serviceMonitorEnabled, ServiceInvocationMonitor... monitors) { this.serviceMonitorEnabled = serviceMonitorEnabled; this.monitor = Arrays.asList(monitors); } public Command<AstrixServiceInvocationResponse> monitorServiceInvocation(Command<AstrixServiceInvocationResponse> execution) { if (!serviceMonitorEnabled.get()) { return execution; } for (ServiceInvocationMonitor monitor : monitor) { execution = monitor.monitor(execution); } return execution; } } private static class PublishedServiceMethod<T> { private final ServiceInvocationMonitors serviceInvocationMonitors; private final Method serviceMethod; private final AstrixObjectSerializer objectSerializer; private final T service; public PublishedServiceMethod(ServiceInvocationMonitors serviceInvocationMonitors, Method method, AstrixObjectSerializer objectSerializer, T service) { this.serviceInvocationMonitors = serviceInvocationMonitors; this.serviceMethod = method; this.objectSerializer = objectSerializer; this.service = service; } private AstrixServiceInvocationResponse timeInvocation(AstrixServiceInvocationRequest request, int version) { return serviceInvocationMonitors.monitorServiceInvocation(() -> invoke(request, version)).call(); } private AstrixServiceInvocationResponse invoke(AstrixServiceInvocationRequest request, int version) { try { return invokeService(request, version); } catch (Exception e) { Throwable exceptionThrownByService = resolveException(e); AstrixServiceInvocationResponse invocationResponse = new AstrixServiceInvocationResponse(); invocationResponse.setExceptionMsg(exceptionThrownByService.getMessage()); invocationResponse.setCorrelationId(UUID.randomUUID().toString()); if (exceptionThrownByService instanceof ServiceInvocationException) { invocationResponse.setException(this.objectSerializer.serialize(exceptionThrownByService, version)); } else { invocationResponse.setThrownExceptionType(exceptionThrownByService.getClass().getName()); } logger.info(String.format("Service invocation ended with exception. request=%s correlationId=%s", request, invocationResponse.getCorrelationId()), exceptionThrownByService); return invocationResponse; } } private AstrixServiceInvocationResponse invokeService(AstrixServiceInvocationRequest request, int version) throws IllegalAccessException, InvocationTargetException { Object[] arguments = unmarshal(request.getArguments(), serviceMethod.getGenericParameterTypes(), version); Object result = serviceMethod.invoke(service, arguments); AstrixServiceInvocationResponse invocationResponse = new AstrixServiceInvocationResponse(); if (serviceMethod.getReturnType().equals(Void.TYPE)) { return invocationResponse; } if (serviceMethod.getReturnType().equals(Optional.class)) { if (result == null) { invocationResponse.setHeader(AstrixServiceInvocationResponseHeaders.OPTIONAL_RETURN_VALUE_IS_NULL, "true"); } else { invocationResponse.setResponseBody(objectSerializer.serialize(Optional.class.cast(result).orElse(null), version)); } } else { invocationResponse.setResponseBody(objectSerializer.serialize(result, version)); } return invocationResponse; } private Object[] unmarshal(Object[] elements, Type[] types, int version) { Object[] result = new Object[elements.length]; for (int i = 0; i < result.length; i++) { result[i] = objectSerializer.deserialize(elements[i], types[i], version); } return result; } } class PublishedService<T> { private final Map<String, PublishedServiceMethod<T>> methodBySignature = new HashMap<>(); /* * Overloaded service methods share the same Metrics */ private final Map<String, ServiceInvocationMonitors> serviceInvocationMonitorsByMethodName = new HashMap<>(); private final AstrixObjectSerializer objectSerializer; private Class<?> providedApi; private ServiceInvocationMonitor serviceMonitor; public PublishedService(T service, AstrixObjectSerializer serializer, Class<?> providedApi) { this.objectSerializer = serializer; this.providedApi = providedApi; // Monitor for service-level metrics (aggregated stats for all methods) this.serviceMonitor = new ServiceInvocationMonitor(metrics.createTimer()); mbeanExporter.registerMBean(this.serviceMonitor, "ExportedServices", providedApi.getName()); for (Method m : providedApi.getMethods()) { ServiceInvocationMonitors serviceInvocationMonitors = serviceInvocationMonitorsByMethodName.computeIfAbsent(m.getName(), this::createServiceInvocationMonitors); methodBySignature.put(ReflectionUtil.methodSignatureWithoutReturnType(m), new PublishedServiceMethod<>(serviceInvocationMonitors, m, objectSerializer, service)); } } private ServiceInvocationMonitors createServiceInvocationMonitors(String methodName) { Timer methodTimer = metrics.createTimer(); // Monitor for method level metrics ServiceInvocationMonitor methodMonitor = new ServiceInvocationMonitor(methodTimer); mbeanExporter.registerMBean(methodMonitor, "ExportedServices", providedApi.getName() + "#" + methodName); ServiceInvocationMonitors result = new ServiceInvocationMonitors(exportedServiceMetricsEnabled, methodMonitor, serviceMonitor, allServicesAggregated); return result; } private AstrixServiceInvocationResponse invoke(AstrixServiceInvocationRequest request, int version, String serviceApi) { String serviceMethodSignature = request.getHeader("serviceMethodSignature"); PublishedServiceMethod<T> serviceMethod = methodBySignature.get(serviceMethodSignature); if (serviceMethod == null) { throw new MissingServiceMethodException(String.format("Missing service method: service=%s method=%s", serviceApi, serviceMethodSignature)); } return serviceMethod.timeInvocation(request, version); } } @Override public void register(Object provider, AstrixObjectSerializer objectSerializer, Class<?> publishedApi) { if (!publishedApi.isAssignableFrom(provider.getClass())) { throw new IllegalArgumentException("Provider: " + provider.getClass() + " does not implement: " + publishedApi); } PublishedService<?> publishedService = new PublishedService<>(provider, objectSerializer, publishedApi); this.serviceByType.put(publishedApi.getName(), publishedService); } /** * @param request * @return */ @Override public AstrixServiceInvocationResponse invokeService(final AstrixServiceInvocationRequest request) { final int version = Integer.parseInt(request.getHeader("apiVersion")); final String serviceApi = request.getHeader("serviceApi"); final PublishedService<?> publishedService = this.serviceByType.get(serviceApi); if (publishedService == null) { /* * Service not available. This might happen in rare conditions when a processing unit * is restarted and old clients connects to the space before the framework is fully initialized. */ AstrixServiceInvocationResponse invocationResponse = new AstrixServiceInvocationResponse(); invocationResponse.setServiceUnavailable(true);; invocationResponse.setExceptionMsg("Service not available in service activator: " + serviceApi); invocationResponse.setCorrelationId(UUID.randomUUID().toString()); logger.info(String.format("Service not available. request=%s correlationId=%s", request, invocationResponse.getCorrelationId())); return invocationResponse; } return publishedService.invoke(request, version, serviceApi); } private static Throwable resolveException(Exception e) { if (e instanceof InvocationTargetException) { // Invoked service threw an exception return InvocationTargetException.class.cast(e).getTargetException(); } return e; } }