/** * Copyright (C) 2014-2015 LinkedIn Corp. (pinot-core@linkedin.com) * * 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.linkedin.pinot.controller.api.restlet.resources; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteStreams; import com.linkedin.pinot.common.metrics.ControllerMeter; import com.linkedin.pinot.common.restlet.swagger.HttpVerb; import com.linkedin.pinot.common.restlet.swagger.Parameter; import com.linkedin.pinot.common.restlet.swagger.Paths; import com.linkedin.pinot.common.restlet.swagger.Response; import com.linkedin.pinot.common.restlet.swagger.Responses; import com.linkedin.pinot.common.restlet.swagger.Summary; import com.linkedin.pinot.common.restlet.swagger.Tags; import com.linkedin.pinot.controller.api.ControllerRestApplication; import com.linkedin.pinot.controller.api.pojos.Instance; import com.linkedin.pinot.controller.helix.core.PinotResourceManagerResponse; import java.util.List; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.helix.model.InstanceConfig; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.restlet.data.MediaType; import org.restlet.data.Status; import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.representation.Variant; import org.restlet.resource.Delete; import org.restlet.resource.Get; import org.restlet.resource.Post; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Restlet to manage Pinot instances. */ public class PinotInstanceRestletResource extends BasePinotControllerRestletResource { private static final Logger LOGGER = LoggerFactory.getLogger(PinotInstanceRestletResource.class); private final ObjectMapper mapper; public PinotInstanceRestletResource() { getVariants().add(new Variant(MediaType.TEXT_PLAIN)); getVariants().add(new Variant(MediaType.APPLICATION_JSON)); setNegotiated(false); mapper = new ObjectMapper(); } @Override @Post("json") public Representation post(Representation entity) { StringRepresentation presentation; try { final String instanceName = (String) getRequest().getAttributes().get(INSTANCE_NAME); if (instanceName == null) { // This is a request to create an instance try { final Instance instance = mapper.readValue(ByteStreams.toByteArray(entity.getStream()), Instance.class); presentation = addInstance(instance); } catch (final Exception e) { presentation = new StringRepresentation(e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e)); LOGGER.error("Caught exception while processing post request", e); ControllerRestApplication.getControllerMetrics() .addMeteredGlobalValue(ControllerMeter.CONTROLLER_INTERNAL_ERROR, 1L); setStatus(Status.SERVER_ERROR_INTERNAL); } } else { // This is a request to toggle the state of an instance if (_pinotHelixResourceManager.instanceExists(instanceName)) { final String state = getRequest().getEntityAsText().trim(); if (isValidState(state)) { presentation = toggleInstanceState(instanceName, state); } else { LOGGER.error(INVALID_STATE_ERROR); setStatus(Status.CLIENT_ERROR_BAD_REQUEST); return new StringRepresentation(INVALID_STATE_ERROR); } } else { setStatus(Status.CLIENT_ERROR_NOT_FOUND); presentation = new StringRepresentation("Error: Instance " + instanceName + " not found."); } } } catch (final Exception e) { presentation = new StringRepresentation(e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e)); LOGGER.error("Caught exception while processing post request", e); ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_INSTANCE_POST_ERROR, 1L); setStatus(Status.SERVER_ERROR_INTERNAL); } return presentation; } @HttpVerb("post") @Summary("Adds an instance") @Tags({ "instance" }) @Paths({ "/instances", "/instances/" }) @Responses({ @Response(statusCode = "200", description = "The instance was created successfully"), @Response(statusCode = "409", description = "The instance already exists and no action was taken"), @Response(statusCode = "500", description = "Failed to create the instance") }) private StringRepresentation addInstance( @Parameter(name = "instance", in = "body", description = "The instance to add", required = true) Instance instance) throws JSONException { StringRepresentation presentation; LOGGER.info("Instance creation request received for instance " + instance.toInstanceId()); final PinotResourceManagerResponse resp = _pinotHelixResourceManager.addInstance(instance); if (resp.status == PinotResourceManagerResponse.ResponseStatus.failure) { setStatus(Status.CLIENT_ERROR_CONFLICT); } presentation = new StringRepresentation(resp.toJSON().toString()); return presentation; } /** * URI mapping: * "/instances", "/instances/" : Lists all the instances * "/instances/{instanceName}" : Gets information about an instance * * {@inheritDoc} * @see org.restlet.resource.ServerResource#get() */ @Override @Get public Representation get() { Representation presentation; try { final String instanceName = (String) getRequest().getAttributes().get(INSTANCE_NAME); if (instanceName == null) { presentation = getAllInstances(); } else { presentation = getInstanceInformation(instanceName); } } catch (final Exception e) { presentation = new StringRepresentation(e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e)); LOGGER.error("Caught exception while processing post request", e); ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_INSTANCE_GET_ERROR, 1L); setStatus(Status.SERVER_ERROR_INTERNAL); } return presentation; } /** * Gets the information for an instance. * * @param instanceName The instance name */ @HttpVerb("get") @Summary("Gets information for an instance") @Tags({ "instance" }) @Paths({ "/instances/{instanceName}", "/instances/{instanceName}/" }) @Responses({ @Response(statusCode = "200", description = "Information about the specified instance"), @Response(statusCode = "404", description = "The specified instance does not exist"), @Response(statusCode = "500", description = "There was an error while fetching information for the given instance") }) private Representation getInstanceInformation( @Parameter( name = "instanceName", description = "The name of the instance (eg. Server_1.2.3.4_1234 or Broker_someHost.example.com_2345)", in = "path", required = true) String instanceName) { try { if (!_pinotHelixResourceManager.instanceExists(instanceName)) { setStatus(Status.CLIENT_ERROR_NOT_FOUND); return new StringRepresentation("Error: Instance " + instanceName + " not found."); } InstanceConfig instanceConfig = _pinotHelixResourceManager.getHelixInstanceConfig(instanceName); JSONObject response = new JSONObject(); response.put("instanceName", instanceConfig.getInstanceName()); response.put("hostName", instanceConfig.getHostName()); response.put("enabled", instanceConfig.getInstanceEnabled()); response.put("port", instanceConfig.getPort()); response.put("tags", new JSONArray(instanceConfig.getTags())); return new StringRepresentation(response.toString()); } catch (Exception e) { LOGGER.warn("Caught exception while fetching information for instance {}", instanceName, e); setStatus(Status.SERVER_ERROR_INTERNAL); return new StringRepresentation("{}"); } } /** * Get all instances in the cluster * @return List of all instances in the cluster. * @throws JSONException */ @HttpVerb("get") @Summary("Views all instances") @Tags({ "instance" }) @Paths({ "/instances", "/instances/" }) @Responses({ @Response(statusCode = "200", description = "A list of instances") }) private Representation getAllInstances() throws JSONException { JSONObject object = new JSONObject(); JSONArray instanceArray = new JSONArray(); List<String> instanceNames = _pinotHelixResourceManager.getAllInstances(); for (String instanceName : instanceNames) { instanceArray.put(instanceName); } object.put("instances", instanceArray); return new StringRepresentation(object.toString()); } /** * * @param instanceName: Name of the instance to enable/disable/drop * @param state: One of '{enable|disable|drop}' * @return StringRepresentation of state after trying to enable/disable/drop instance. * @throws JSONException */ @HttpVerb("post") @Summary("Enable, disable or drop an instance") @Tags({ "instance" }) @Paths({ "/instances/{instanceName}/state", "/instances/{instanceName}/state" }) @Responses({ @Response(statusCode = "200", description = "The instance state was changed successfully"), @Response(statusCode = "400", description = "The state given was not enable, disable or drop"), @Response(statusCode = "404", description = "The instance was not found") }) private StringRepresentation toggleInstanceState( @Parameter(name = "instanceName", in = "path", description = "The name of the instance for which to toggle its state", required = true) String instanceName, @Parameter(name = "state", in = "body", description = "The desired instance state, either enable, disable or drop", required = true) String state) throws JSONException { if (StateType.ENABLE.name().equalsIgnoreCase(state)) { return new StringRepresentation(_pinotHelixResourceManager.enableInstance(instanceName).toJSON().toString()); } else if (StateType.DISABLE.name().equalsIgnoreCase(state)) { return new StringRepresentation(_pinotHelixResourceManager.disableInstance(instanceName).toJSON().toString()); } else if (StateType.DROP.name().equalsIgnoreCase(state)) { return new StringRepresentation(_pinotHelixResourceManager.dropInstance(instanceName).toJSON().toString()); } else { LOGGER.error(INVALID_INSTANCE_URI_ERROR); setStatus(Status.CLIENT_ERROR_BAD_REQUEST); return new StringRepresentation(INVALID_INSTANCE_URI_ERROR); } } @Override @Delete public Representation delete() { Representation presentation; try { final String instanceName = (String) getRequest().getAttributes().get(INSTANCE_NAME); presentation = deleteInstanceInformation(instanceName); } catch (final Exception e) { presentation = exceptionToStringRepresentation(e); LOGGER.error("Caught exception while deleting an instance ", e); ControllerRestApplication.getControllerMetrics().addMeteredGlobalValue(ControllerMeter.CONTROLLER_INSTANCE_DELETE_ERROR, 1L); setStatus(Status.SERVER_ERROR_INTERNAL); } return presentation; } /** * Deletes an instance * * @param instanceName */ @HttpVerb("delete") @Summary("Deletes an instance") @Tags({ "instance" }) @Paths({ "/instances/{instanceName}", "/instances/{instanceName}/" }) @Responses({ @Response(statusCode = "200", description = "The instance has been deleted successfully"), @Response(statusCode = "404", description = "The specified instance does not exist"), @Response(statusCode = "409", description = "Forbidden operation typically because the instance is live or " + "idealstates still contain some information of this instance \n"), @Response(statusCode = "500", description = "There was an error while cleaning files for the instance.") }) private Representation deleteInstanceInformation( @Parameter( name = "instanceName", description = "The name of the instance (eg. Server_1.2.3.4_1234 or Broker_someHost.example.com_2345)", in = "path", required = true) String instanceName) { try { PinotResourceManagerResponse response; // Check that the user input for an instance is correct if (!_pinotHelixResourceManager.instanceExists(instanceName)) { setStatus(Status.CLIENT_ERROR_NOT_FOUND); response = new PinotResourceManagerResponse("Instance " + instanceName + " does not exist.", false); return new StringRepresentation(response.toJSON().toString()); } // Check that the instance is safe to drop if (!_pinotHelixResourceManager.isInstanceDroppable(instanceName)) { setStatus(Status.CLIENT_ERROR_CONFLICT); response = new PinotResourceManagerResponse("Instance " + instanceName + " is live or it still appears in the idealstate.", false); return new StringRepresentation(response.toJSON().toString()); } // Delete the instance information from Helix storage response = _pinotHelixResourceManager.dropInstance(instanceName); if (!response.isSuccessful()) { setStatus(Status.SERVER_ERROR_INTERNAL); } return new StringRepresentation(response.toJSON().toString()); } catch (Exception e) { LOGGER.warn("Caught exception while deleting information for instance {}", instanceName, e); setStatus(Status.SERVER_ERROR_INTERNAL); return new StringRepresentation("Caught exception while deleting information for instance " + instanceName); } } }