package io.scalecube.services;
import io.scalecube.services.routing.Router;
import io.scalecube.transport.Message;
import io.scalecube.transport.Message.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class ServiceCall {
private static final Logger LOGGER = LoggerFactory.getLogger(ServiceProxyFactory.class);
/**
* used to complete the request future with timeout exception in case no response comes from service.
*/
private static final ScheduledExecutorService delayer =
ThreadFactory.singleScheduledExecutorService("sc-services-timeout");
private Duration timeout;
private Router router;
public ServiceCall(Router router, Duration timeout) {
this.router = router;
this.timeout = timeout;
}
public <T> CompletableFuture<Message> invoke(Message message) {
return invoke(message, timeout);
}
/**
* Dispatch a request message and invoke a service by a given service name and method name. expected headers in
* request: ServiceHeaders.SERVICE_REQUEST the logical name of the service. ServiceHeaders.METHOD the method name to
* invoke message uses the router to select the target endpoint service instance in the cluster.
*
* @param request request with given headers.
* @timeout duration of the response before TimeException is returned.
* @return CompletableFuture with service call dispatching result.
* @throws Exception in case of an error or TimeoutException if no response if a given duration.
*/
public <T> CompletableFuture<Message> invoke(Message request, Duration timeout) {
String serviceName = request.header(ServiceHeaders.SERVICE_REQUEST);
String methodName = request.header(ServiceHeaders.METHOD);
try {
Optional<ServiceInstance> optionalServiceInstance = router.route(request);
if (optionalServiceInstance.isPresent()) {
return this.invoke(request, optionalServiceInstance.get(), timeout);
} else {
LOGGER.error(
"Failed to invoke service, No reachable member with such service definition [{}], args [{}]",
serviceName, request);
throw new IllegalStateException("No reachable member with such service: " + methodName);
}
} catch (Throwable ex) {
LOGGER.error(
"Failed to invoke service, No reachable member with such service method [{}], args [{}], error [{}]",
methodName, request.data(), ex);
throw new IllegalStateException("No reachable member with such service: " + methodName);
}
}
/**
* Dispatch a request message and invoke a service by a given service name and method name. expected headers in
* request: ServiceHeaders.SERVICE_REQUEST the logical name of the service. ServiceHeaders.METHOD the method name to
* invoke with default timeout.
*
* @param request request with given headers.
* @param serviceInstance target instance to invoke.
* @return CompletableFuture with service call dispatching result.
* @throws Exception in case of an error or TimeoutException if no response if a given duration.
*/
public <T> CompletableFuture<Message> invoke(Message request, ServiceInstance serviceInstance) throws Exception {
return invoke(request, serviceInstance, timeout);
}
/**
* Dispatch a request message and invoke a service by a given service name and method name. expected headers in
* request: ServiceHeaders.SERVICE_REQUEST the logical name of the service. ServiceHeaders.METHOD the method name to
* invoke.
*
* @param request request with given headers.
* @param serviceInstance target instance to invoke.
* @param duration of the response before TimeException is returned.
* @return CompletableFuture with service call dispatching result.
* @throws Exception in case of an error or TimeoutException if no response if a given duration.
*/
@SuppressWarnings("unchecked")
public <T> CompletableFuture<Message> invoke(Message request, ServiceInstance serviceInstance, Duration duration)
throws Exception {
if (serviceInstance.isLocal()) {
CompletableFuture<?> resultFuture = (CompletableFuture<?>) serviceInstance.invoke(request);
return (CompletableFuture<Message>) timeoutAfter(resultFuture, timeout)
.thenApply(result -> toMessage(request, (T) result));
} else {
RemoteServiceInstance remote = (RemoteServiceInstance) serviceInstance;
CompletableFuture<?> resultFuture =
(CompletableFuture<?>) remote.dispatch(request);
return (CompletableFuture<Message>) timeoutAfter(resultFuture, timeout);
}
}
private <T> Message toMessage(Message request, T result) {
if (result instanceof Message) {
return (Message) result;
} else {
return Message.builder()
.header(ServiceHeaders.SERVICE_RESPONSE, request.header(ServiceHeaders.SERVICE_REQUEST))
.header(ServiceHeaders.METHOD, request.header(ServiceHeaders.METHOD))
.correlationId(request.correlationId())
.qualifier(request.qualifier())
.data(result)
.build();
}
}
private CompletableFuture<?> timeoutAfter(final CompletableFuture<?> resultFuture, Duration timeout) {
final CompletableFuture<Class<Void>> timeoutFuture = new CompletableFuture<>();
// schedule to terminate the target goal in future in case it was not done yet
final ScheduledFuture<?> scheduledEvent = delayer.schedule(() -> {
// by this time the target goal should have finished.
if (!resultFuture.isDone()) {
// target goal not finished in time so cancel it with timeout.
resultFuture.completeExceptionally(new TimeoutException("expecting response reached timeout!"));
}
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
// cancel the timeout in case target goal did finish on time
if (resultFuture != null) {
resultFuture.thenRun(() -> {
if (resultFuture.isDone()) {
if (!scheduledEvent.isDone()) {
scheduledEvent.cancel(false);
}
timeoutFuture.complete(Void.TYPE);
}
});
} else {
return CompletableFuture.completedFuture(null);
}
return resultFuture;
}
/**
* helper method to get service request builder with needed headers.
*
* @param serviceName the requested service name.
* @param methodName the requested service method name.
* @return Builder for requested message.
*/
public static Builder request(String serviceName, String methodName) {
return Message.builder()
.header(ServiceHeaders.SERVICE_REQUEST, serviceName)
.header(ServiceHeaders.METHOD, methodName);
}
}