com.vmware.xenon.gateway.GatewayService Maven / Gradle / Ivy
/*
* Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
*
* 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.vmware.xenon.gateway;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.logging.Level;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.ServiceDocument;
import com.vmware.xenon.common.ServiceErrorResponse;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.common.Utils;
/**
* This class represents a data driven, highly-available Gateway service,
* used to intercept and filter http requests for a set of backend nodes.
* Filtering of requests is performed through a set of white-listed URI paths and
* allowed http actions.
*
* Configuration policies for the GatewayService are stored using different
* Stateful Replicated services. See: {@link GatewayConfigService} and
* {@link GatewayPathService}. Users will interact with these services directly when
* updating state for a specific gateway.
*
* For maximum through-put, the Gateway Service uses an in-memory cache for all
* configuration policies {@link GatewayCache}. The cache is updated
* asynchronously as soon as configuration is added/updated through the services
* listed above. The cached state can be queried from the gateway service by making
* a HTTP GET request on the gateway-service self-link. This can be used to ensure
* that the ingested configuration is now Active.
*/
public class GatewayService extends StatelessService {
public static final String SELF_LINK = "/";
public static final String PROPERTY_NAME_GATEWAY_SERVICE_LOGGING = Utils.PROPERTY_NAME_PREFIX
+ "gatewayService.isRequestLoggingEnabled";
private GatewayCache cache;
private URI configHostUri;
private final boolean isRequestLoggingEnabled = Boolean
.getBoolean(PROPERTY_NAME_GATEWAY_SERVICE_LOGGING);
public GatewayService(URI configHostUri) {
super(ServiceDocument.class);
super.toggleOption(ServiceOption.URI_NAMESPACE_OWNER, true);
this.configHostUri = configHostUri;
}
@Override
public void handleStart(Operation startOp) {
try {
// Initialize the cache. Setting up subscriptions allows
// the cache to stay uptodate as configuration is
// updated.
this.cache = GatewayCache.create(
getHost(), this.configHostUri, GatewayUriPaths.DEFAULT_CONFIG_PATH);
this.cache.start((t) -> {
if (t != null) {
startOp.fail(t);
return;
}
startOp.complete();
});
} catch (Throwable t) {
startOp.fail(t);
}
}
/**
* The main gateway routine, called for each intercepted request.
*/
@Override
public void handleRequest(Operation op) {
String path = op.getUri().getPath();
// If the request is for the Gateway endpoint
// use regular request handling flow.
if (path.equals("") || path.equals(SELF_LINK)) {
super.handleRequest(op);
return;
}
final long startTimeMicros = Utils.getSystemNowMicrosUtc();
// Fail the request if the gateway is marked as
// UN-AVAILABLE.
if (this.cache.getGatewayStatus() == GatewayStatus.UNAVAILABLE) {
failRequest(op, startTimeMicros, Operation.STATUS_CODE_UNAVAILABLE,
"Gateway is currently unavailable. Please retry later.");
return;
}
if (this.cache.filterRequests()) {
// Check if the requested path exists in our allowed uris.
EnumSet actions = this.cache.getSupportedActions(path);
if (actions == null) {
// If not, this could be a child-service request. Get
// the parent path and check if exists in allowed uris.
path = UriUtils.getParentPath(path);
actions = this.cache.getSupportedActions(path);
if (actions == null) {
failRequest(op, startTimeMicros, Operation.STATUS_CODE_NOT_FOUND,
"Requested path %s not found.", path);
return;
}
}
// Check if the requested Action is allowed on the requested path.
if (!actions.contains(op.getAction())) {
failRequest(op, startTimeMicros, Operation.STATUS_CODE_BAD_METHOD,
"Requested verb %s not allowed on path %s.", op.getAction(), path);
return;
}
}
// Check if the Gateway has been PAUSED. If so, queue the operation.
if (this.cache.getGatewayStatus() == GatewayStatus.PAUSED) {
failRequest(op, startTimeMicros, Operation.STATUS_CODE_UNAVAILABLE,
"Gateway is currently PAUSED. Please retry later.");
return;
}
// Select one available node randomly. If there are no backend
// nodes that are currently available, simply fail the request.
URI nodeAddress = this.cache.getForwardingUri();
if (nodeAddress == null) {
failRequest(op, startTimeMicros, Operation.STATUS_CODE_UNAVAILABLE,
"Gateway is currently unavailable. Please retry later.");
return;
}
// Forward the request to the selected backend node.
Operation outboundOp = op.clone();
outboundOp.setUri(createNewUri(nodeAddress, op.getUri()));
outboundOp.forceRemote();
outboundOp.setCompletion((o, e) -> {
op.transferResponseHeadersFrom(o);
op.setStatusCode(o.getStatusCode());
op.setContentType(o.getContentType());
op.setContentLength(o.getContentLength());
op.setBodyNoCloning(o.getBodyRaw());
if (e != null) {
op.fail(e);
} else {
op.complete();
}
logRequest(op, startTimeMicros, o.getStatusCode(), o.getContentLength());
});
getHost().sendRequest(outboundOp);
}
/**
* Called when a GET is issued on the self-link of the
* GatewayService. This method returns the cached state of the
* Gateway.
*/
@Override
public void handleGet(Operation op) {
op.setBodyNoCloning(this.cache.getGatewayState());
op.complete();
}
/**
* DELETE on the GatewayService self-link are not-allowed.
* This would otherwise cause the GatewayService to stop.
*/
@Override
public void handleDelete(Operation op) {
long startTimeMicros = Utils.getSystemNowMicrosUtc();
if (!op.hasPragmaDirective(Operation.PRAGMA_DIRECTIVE_NO_INDEX_UPDATE)) {
failRequest(op, startTimeMicros, Operation.STATUS_CODE_BAD_METHOD,
"DELETE not supported on Gateway endpoint.");
return;
}
op.complete();
}
/**
* Called when the GatewayService is stopped by the ServiceHost.
*/
@Override
public void handleStop(Operation op) {
this.cache.stop();
op.complete();
}
private URI createNewUri(URI nodeAddress, URI opUri) {
try {
return new URI(
nodeAddress.getScheme(), opUri.getUserInfo(), nodeAddress.getHost(),
nodeAddress.getPort(), opUri.getPath(), opUri.getQuery(), opUri.getFragment());
} catch (URISyntaxException x) {
throw new IllegalArgumentException(x.getMessage(), x);
}
}
private void failRequest(Operation op, long startTimeMicros,
int statusCode, String msgFormat, Object... args) {
ServiceErrorResponse rsp = new ServiceErrorResponse();
rsp.message = String.format(msgFormat, args);
rsp.statusCode = statusCode;
op.fail(statusCode, new IllegalStateException(rsp.message), rsp);
logRequest(op, startTimeMicros, statusCode, op.getContentLength());
}
// Requests are logged in the following format:
// Action - Requested UriPath - StatusCode - ContentLength (in bytes) - Time taken (in milliseconds)
// Sample: POST /core/examples 200 416B 11.00ms
private void logRequest(Operation op, long startTimeMicros, int statusCode, long contentLength) {
if (this.isRequestLoggingEnabled) {
long endTimeMicros = Utils.getSystemNowMicrosUtc();
Level level = op.getStatusCode() < Operation.STATUS_CODE_FAILURE_THRESHOLD ?
Level.INFO : Level.WARNING;
log(level, "%s %s %d %dB %.2fms", op.getAction(), op.getUri().getPath(),
statusCode, contentLength, (endTimeMicros - startTimeMicros) / 1000.0);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy