/* * Copyright © 2014-2016 Cask Data, Inc. * * 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 co.cask.cdap.gateway.handlers; import co.cask.cdap.api.ProgramSpecification; import co.cask.cdap.api.app.ApplicationSpecification; import co.cask.cdap.api.flow.FlowSpecification; import co.cask.cdap.api.flow.FlowletDefinition; import co.cask.cdap.api.metrics.MetricStore; import co.cask.cdap.api.schedule.ScheduleSpecification; import co.cask.cdap.api.service.ServiceSpecification; import co.cask.cdap.app.mapreduce.MRJobInfoFetcher; import co.cask.cdap.app.runtime.ProgramController; import co.cask.cdap.app.runtime.ProgramRuntimeService; import co.cask.cdap.app.store.Store; import co.cask.cdap.common.BadRequestException; import co.cask.cdap.common.ConflictException; import co.cask.cdap.common.MethodNotAllowedException; import co.cask.cdap.common.NotFoundException; import co.cask.cdap.common.NotImplementedException; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.io.CaseInsensitiveEnumTypeAdapterFactory; import co.cask.cdap.config.PreferencesStore; import co.cask.cdap.data2.transaction.queue.QueueAdmin; import co.cask.cdap.gateway.handlers.util.AbstractAppFabricHttpHandler; import co.cask.cdap.internal.app.ApplicationSpecificationAdapter; import co.cask.cdap.internal.app.runtime.flow.FlowUtils; import co.cask.cdap.internal.app.runtime.schedule.Scheduler; import co.cask.cdap.internal.app.runtime.schedule.SchedulerException; import co.cask.cdap.internal.app.services.ProgramLifecycleService; import co.cask.cdap.internal.app.store.RunRecordMeta; import co.cask.cdap.proto.BatchProgram; import co.cask.cdap.proto.BatchProgramResult; import co.cask.cdap.proto.BatchProgramStart; import co.cask.cdap.proto.BatchProgramStatus; import co.cask.cdap.proto.BatchRunnable; import co.cask.cdap.proto.BatchRunnableInstances; import co.cask.cdap.proto.Containers; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.Instances; import co.cask.cdap.proto.MRJobInfo; import co.cask.cdap.proto.NotRunningProgramLiveInfo; import co.cask.cdap.proto.ProgramLiveInfo; import co.cask.cdap.proto.ProgramRecord; import co.cask.cdap.proto.ProgramRunStatus; import co.cask.cdap.proto.ProgramStatus; import co.cask.cdap.proto.ProgramType; import co.cask.cdap.proto.RunRecord; import co.cask.cdap.proto.ServiceInstances; import co.cask.cdap.proto.id.Ids; import co.cask.cdap.proto.id.NamespaceId; import co.cask.cdap.proto.id.ProgramId; import co.cask.cdap.proto.id.ProgramRunId; import co.cask.http.HttpResponder; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Singleton; import org.jboss.netty.buffer.ChannelBufferInputStream; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; /** * {@link co.cask.http.HttpHandler} to manage program lifecycle for v3 REST APIs */ @Singleton @Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}") public class ProgramLifecycleHttpHandler extends AbstractAppFabricHttpHandler { private static final Logger LOG = LoggerFactory.getLogger(ProgramLifecycleHttpHandler.class); private static final Type BATCH_PROGRAMS_TYPE = new TypeToken<List<BatchProgram>>() { }.getType(); private static final Type BATCH_RUNNABLES_TYPE = new TypeToken<List<BatchRunnable>>() { }.getType(); private static final Type BATCH_STARTS_TYPE = new TypeToken<List<BatchProgramStart>>() { }.getType(); /** * Json serializer/deserializer. */ private static final Gson GSON = ApplicationSpecificationAdapter .addTypeAdapters(new GsonBuilder()) .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()) .create(); private static final Function<RunRecordMeta, RunRecord> CONVERT_TO_RUN_RECORD = new Function<RunRecordMeta, RunRecord>() { @Override public RunRecord apply(RunRecordMeta input) { return new RunRecord(input); } }; private final ProgramLifecycleService lifecycleService; private final QueueAdmin queueAdmin; private final PreferencesStore preferencesStore; private final MetricStore metricStore; private final MRJobInfoFetcher mrJobInfoFetcher; /** * Store manages non-runtime lifecycle. */ protected final Store store; /** * Runtime program service for running and managing programs. */ protected final ProgramRuntimeService runtimeService; /** * Scheduler provides ability to schedule/un-schedule the jobs. */ protected final Scheduler scheduler; @Inject ProgramLifecycleHttpHandler(Store store, ProgramRuntimeService runtimeService, ProgramLifecycleService lifecycleService, QueueAdmin queueAdmin, Scheduler scheduler, PreferencesStore preferencesStore, MRJobInfoFetcher mrJobInfoFetcher, MetricStore metricStore) { this.store = store; this.runtimeService = runtimeService; this.lifecycleService = lifecycleService; this.metricStore = metricStore; this.queueAdmin = queueAdmin; this.scheduler = scheduler; this.preferencesStore = preferencesStore; this.mrJobInfoFetcher = mrJobInfoFetcher; } /** * Relays job-level and task-level information about a particular MapReduce program run. */ @GET @Path("/apps/{app-id}/mapreduce/{mapreduce-id}/runs/{run-id}/info") public void getMapReduceInfo(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("mapreduce-id") String mapreduceId, @PathParam("run-id") String runId) throws IOException, NotFoundException { Id.Program programId = Id.Program.from(namespaceId, appId, ProgramType.MAPREDUCE, mapreduceId); Id.Run run = new Id.Run(programId, runId); ApplicationSpecification appSpec = store.getApplication(programId.getApplication()); if (appSpec == null) { throw new NotFoundException(programId.getApplication()); } if (!appSpec.getMapReduce().containsKey(mapreduceId)) { throw new NotFoundException(programId); } RunRecordMeta runRecordMeta = store.getRun(programId, runId); if (runRecordMeta == null) { throw new NotFoundException(run); } MRJobInfo mrJobInfo = mrJobInfoFetcher.getMRJobInfo(run); mrJobInfo.setState(runRecordMeta.getStatus().name()); // Multiple startTs / endTs by 1000, to be consistent with Task-level start/stop times returned by JobClient // in milliseconds. RunRecord returns seconds value. mrJobInfo.setStartTime(TimeUnit.SECONDS.toMillis(runRecordMeta.getStartTs())); Long stopTs = runRecordMeta.getStopTs(); if (stopTs != null) { mrJobInfo.setStopTime(TimeUnit.SECONDS.toMillis(stopTs)); } // JobClient (in DistributedMRJobInfoFetcher) can return NaN as some of the values, and GSON otherwise fails Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); responder.sendJson(HttpResponseStatus.OK, mrJobInfo, mrJobInfo.getClass(), gson); } /** * Returns status of a type specified by the type{flows,workflows,mapreduce,spark,services,schedules}. */ @GET @Path("/apps/{app-id}/{type}/{id}/status") public void getStatus(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("type") String type, @PathParam("id") String id) throws NotFoundException, SchedulerException, BadRequestException { if (type.equals("schedules")) { getScheduleStatus(responder, appId, namespaceId, id); return; } ProgramType programType; try { programType = ProgramType.valueOfCategoryName(type); } catch (IllegalArgumentException e) { throw new BadRequestException(e); } ProgramId program = Ids.namespace(namespaceId).app(appId).program(programType, id); ProgramStatus programStatus = lifecycleService.getProgramStatus(program); Map<String, String> status = ImmutableMap.of("status", programStatus.name()); responder.sendJson(HttpResponseStatus.OK, status); } private void getScheduleStatus(HttpResponder responder, String appId, String namespaceId, String scheduleName) throws NotFoundException, SchedulerException { Id.Application applicationId = Id.Application.from(namespaceId, appId); ApplicationSpecification appSpec = store.getApplication(applicationId); if (appSpec == null) { throw new NotFoundException(applicationId); } ScheduleSpecification scheduleSpec = appSpec.getSchedules().get(scheduleName); if (scheduleSpec == null) { throw new NotFoundException(scheduleName, String.format("Schedule: %s for application: %s", scheduleName, applicationId.getId())); } String programName = scheduleSpec.getProgram().getProgramName(); ProgramType programType = ProgramType.valueOfSchedulableType(scheduleSpec.getProgram().getProgramType()); Id.Program programId = Id.Program.from(namespaceId, appId, programType, programName); JsonObject json = new JsonObject(); json.addProperty("status", scheduler.scheduleState(programId, programId.getType().getSchedulableType(), scheduleName).toString()); responder.sendJson(HttpResponseStatus.OK, json); } /** * Stops the particular run of the Workflow or MapReduce program. */ @POST @Path("/apps/{app-id}/{type}/{id}/runs/{run-id}/stop") public void performRunLevelStop(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("type") String type, @PathParam("id") String id, @PathParam("run-id") String runId) throws Exception { ProgramType programType; try { programType = ProgramType.valueOfCategoryName(type); } catch (IllegalArgumentException e) { throw new BadRequestException(e); } ProgramId program = Ids.namespace(namespaceId).app(appId).program(programType, id); lifecycleService.stop(program, runId); responder.sendStatus(HttpResponseStatus.OK); } @POST @Path("/apps/{app-id}/{type}/{id}/{action}") public void performAction(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("type") String type, @PathParam("id") String id, @PathParam("action") String action) throws Exception { if ("schedules".equals(type)) { suspendResumeSchedule(responder, namespaceId, appId, id, action); return; } ProgramType programType; try { programType = ProgramType.valueOfCategoryName(type); } catch (IllegalArgumentException e) { throw new BadRequestException(String.format("Unknown program type '%s'", type), e); } ProgramId programId = Ids.namespace(namespaceId).app(appId).program(programType, id); Map<String, String> args = decodeArguments(request); // we have already validated that the action is valid switch (action.toLowerCase()) { case "start": lifecycleService.start(programId, args, false); break; case "debug": if (!isDebugAllowed(programType)) { throw new NotImplementedException(String.format("debug action is not implemented for program type %s", programType)); } lifecycleService.start(programId, args, true); break; case "stop": lifecycleService.stop(programId); break; default: throw new NotFoundException(String.format("%s action was not found", action)); } responder.sendStatus(HttpResponseStatus.OK); } private void suspendResumeSchedule(HttpResponder responder, String namespaceId, String appId, String scheduleName, String action) throws SchedulerException { try { if (!action.equals("suspend") && !action.equals("resume")) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "Schedule can only be suspended or resumed."); return; } ApplicationSpecification appSpec = store.getApplication(Id.Application.from(namespaceId, appId)); if (appSpec == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "App: " + appId + " not found"); return; } ScheduleSpecification scheduleSpec = appSpec.getSchedules().get(scheduleName); if (scheduleSpec == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Schedule: " + scheduleName + " not found"); return; } String programName = scheduleSpec.getProgram().getProgramName(); ProgramType programType = ProgramType.valueOfSchedulableType(scheduleSpec.getProgram().getProgramType()); Id.Program programId = Id.Program.from(namespaceId, appId, programType, programName); Scheduler.ScheduleState state = scheduler.scheduleState(programId, scheduleSpec.getProgram().getProgramType(), scheduleName); switch (state) { case NOT_FOUND: responder.sendStatus(HttpResponseStatus.NOT_FOUND); break; case SCHEDULED: if (action.equals("suspend")) { scheduler.suspendSchedule(programId, scheduleSpec.getProgram().getProgramType(), scheduleName); responder.sendJson(HttpResponseStatus.OK, "OK"); } else { // attempt to resume already resumed schedule responder.sendJson(HttpResponseStatus.CONFLICT, "Already resumed"); } break; case SUSPENDED: if (action.equals("suspend")) { // attempt to suspend already suspended schedule responder.sendJson(HttpResponseStatus.CONFLICT, "Schedule already suspended"); } else { scheduler.resumeSchedule(programId, scheduleSpec.getProgram().getProgramType(), scheduleName); responder.sendJson(HttpResponseStatus.OK, "OK"); } break; } } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } catch (NotFoundException e) { responder.sendString(HttpResponseStatus.NOT_FOUND, e.getMessage()); } } /** * Returns program runs based on options it returns either currently running or completed or failed. * Default it returns all. */ @GET @Path("/apps/{app-id}/{program-type}/{program-id}/runs") public void programHistory(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("program-type") String programType, @PathParam("program-id") String programId, @QueryParam("status") String status, @QueryParam("start") String startTs, @QueryParam("end") String endTs, @QueryParam("limit") @DefaultValue("100") final int resultLimit) throws BadRequestException, NotFoundException { ProgramType type = getProgramType(programType); if (type == null || type == ProgramType.WEBAPP) { throw new NotFoundException(String.format("Program history is not supported for program type '%s'.", programType)); } long start = (startTs == null || startTs.isEmpty()) ? 0 : Long.parseLong(startTs); long end = (endTs == null || endTs.isEmpty()) ? Long.MAX_VALUE : Long.parseLong(endTs); getRuns(responder, Id.Program.from(namespaceId, appId, type, programId), status, start, end, resultLimit); } /** * Returns run record for a particular run of a program. */ @GET @Path("/apps/{app-id}/{program-type}/{program-id}/runs/{run-id}") public void programRunRecord(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("program-type") String programType, @PathParam("program-id") String programId, @PathParam("run-id") String runid) throws NotFoundException { ProgramType type = getProgramType(programType); if (type == null || type == ProgramType.WEBAPP) { throw new NotFoundException(String.format("Program run record is not supported for program type '%s'.", programType)); } Id.Program progId = Id.Program.from(namespaceId, appId, type, programId); RunRecordMeta runRecordMeta = store.getRun(progId, runid); if (runRecordMeta != null) { RunRecord runRecord = CONVERT_TO_RUN_RECORD.apply(runRecordMeta); responder.sendJson(HttpResponseStatus.OK, runRecord); return; } throw new NotFoundException(new ProgramRunId(namespaceId, appId, type, programId, runid)); } /** * Get program runtime args. */ @GET @Path("/apps/{app-id}/{program-type}/{program-id}/runtimeargs") public void getProgramRuntimeArgs(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("program-type") String programType, @PathParam("program-id") String programId) throws BadRequestException, NotImplementedException, NotFoundException { ProgramType type = getProgramType(programType); if (type == null || type == ProgramType.WEBAPP) { throw new NotFoundException(String.format("Getting program runtime arguments is not supported for program " + "type '%s'.", programType)); } Id.Program id = Id.Program.from(namespaceId, appId, type, programId); if (!store.programExists(id)) { throw new NotFoundException(id); } Map<String, String> runtimeArgs = preferencesStore.getProperties(id.getNamespaceId(), appId, programType, programId); responder.sendJson(HttpResponseStatus.OK, runtimeArgs); } /** * Save program runtime args. */ @PUT @Path("/apps/{app-id}/{program-type}/{program-id}/runtimeargs") public void saveProgramRuntimeArgs(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("program-type") String programType, @PathParam("program-id") String programId) throws Exception { ProgramType type = getProgramType(programType); if (type == null || type == ProgramType.WEBAPP) { throw new NotFoundException(String.format("Saving program runtime arguments is not supported for program " + "type '%s'.", programType)); } lifecycleService.saveRuntimeArgs(Ids.namespace(namespaceId).app(appId).program(type, programId), decodeArguments(request)); responder.sendStatus(HttpResponseStatus.OK); } @GET @Path("/apps/{app-id}/{program-type}/{program-id}") public void programSpecification(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("program-type") String programType, @PathParam("program-id") String programId) throws Exception { ProgramType type = getProgramType(programType); if (type == null) { throw new MethodNotAllowedException(request.getMethod(), request.getUri()); } ProgramId id = Ids.namespace(namespaceId).app(appId).program(type, programId); ProgramSpecification specification = lifecycleService.getProgramSpecification(id); if (specification == null) { throw new NotFoundException(programId); } responder.sendJson(HttpResponseStatus.OK, specification); } /** * Returns the status for all programs that are passed into the data. The data is an array of JSON objects * where each object must contain the following three elements: appId, programType, and programId * (flow name, service name, etc.). * <p> * Example input: * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1"}, * {"appId": "App1", "programType": "Mapreduce", "programId": "MapReduce2"}, * {"appId": "App2", "programType": "Flow", "programId": "Flow1"}] * </code></pre> * </p><p> * The response will be an array of JsonObjects each of which will contain the three input parameters * as well as 2 fields, "status" which maps to the status of the program and "statusCode" which maps to the * status code for the data in that JsonObjects. * </p><p> * If an error occurs in the input (for the example above, App2 does not exist), then all JsonObjects for which the * parameters have a valid status will have the status field but all JsonObjects for which the parameters do not have * a valid status will have an error message and statusCode. * </p><p> * For example, if there is no App2 in the data above, then the response would be 200 OK with following possible data: * </p> * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1", "statusCode": 200, "status": "RUNNING"}, * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 200, "status": "STOPPED"}, * {"appId":"App2", "programType":"Flow", "programId":"Flow1", "statusCode":404, "error": "App: App2 not found"}] * </code></pre> */ @POST @Path("/status") public void getStatuses(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws IOException, BadRequestException { List<BatchProgram> programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); List<BatchProgramStatus> statuses = new ArrayList<>(programs.size()); for (BatchProgram program : programs) { ProgramId programId = Ids.namespace(namespaceId).app(program.getAppId()).program(program.getProgramType(), program.getProgramId()); try { ProgramStatus programStatus = lifecycleService.getProgramStatus(programId); statuses.add(new BatchProgramStatus( program, HttpResponseStatus.OK.getCode(), null, programStatus.name())); } catch (NotFoundException e) { statuses.add(new BatchProgramStatus( program, HttpResponseStatus.NOT_FOUND.getCode(), e.getMessage(), null)); } } responder.sendJson(HttpResponseStatus.OK, statuses); } /** * Stops all programs that are passed into the data. The data is an array of JSON objects * where each object must contain the following three elements: appId, programType, and programId * (flow name, service name, etc.). * <p> * Example input: * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1"}, * {"appId": "App1", "programType": "Mapreduce", "programId": "MapReduce2"}, * {"appId": "App2", "programType": "Flow", "programId": "Flow1"}] * </code></pre> * </p><p> * The response will be an array of JsonObjects each of which will contain the three input parameters * as well as a "statusCode" field which maps to the status code for the data in that JsonObjects. * </p><p> * If an error occurs in the input (for the example above, App2 does not exist), then all JsonObjects for which the * parameters have a valid status will have the status field but all JsonObjects for which the parameters do not have * a valid status will have an error message and statusCode. * </p><p> * For example, if there is no App2 in the data above, then the response would be 200 OK with following possible data: * </p> * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1", "statusCode": 200}, * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 200}, * {"appId":"App2", "programType":"Flow", "programId":"Flow1", "statusCode":404, "error": "App: App2 not found"}] * </code></pre> */ @POST @Path("/stop") public void stopPrograms(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { List<BatchProgram> programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); List<ListenableFuture<BatchProgramResult>> issuedStops = new ArrayList<>(programs.size()); for (final BatchProgram program : programs) { ProgramId programId = Ids.namespace(namespaceId).app(program.getAppId()).program(program.getProgramType(), program.getProgramId()); try { ListenableFuture<BatchProgramResult> issuedStop = Futures.transform(lifecycleService.issueStop(programId, null), new Function<ProgramController, BatchProgramResult>() { @Override public BatchProgramResult apply(ProgramController input) { return new BatchProgramResult(program, HttpResponseStatus.OK.getCode(), null); } }); issuedStops.add(issuedStop); } catch (NotFoundException e) { issuedStops.add(Futures.immediateFuture( new BatchProgramResult(program, HttpResponseStatus.NOT_FOUND.getCode(), e.getMessage()))); } catch (BadRequestException e) { issuedStops.add(Futures.immediateFuture( new BatchProgramResult(program, HttpResponseStatus.BAD_REQUEST.getCode(), e.getMessage()))); } } List<BatchProgramResult> output = new ArrayList<>(programs.size()); // need to keep this index in case there is an exception getting the future, since we won't have the program // information in that scenario int i = 0; for (ListenableFuture<BatchProgramResult> issuedStop : issuedStops) { try { output.add(issuedStop.get()); } catch (Throwable t) { LOG.warn(t.getMessage(), t); output.add(new BatchProgramResult(programs.get(i), HttpResponseStatus.INTERNAL_SERVER_ERROR.getCode(), t.getMessage())); } i++; } responder.sendJson(HttpResponseStatus.OK, output); } /** * Starts all programs that are passed into the data. The data is an array of JSON objects * where each object must contain the following three elements: appId, programType, and programId * (flow name, service name, etc.). In additional, each object can contain an optional runtimeargs element, * which is a map of arguments to start the program with. * <p> * Example input: * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1"}, * {"appId": "App1", "programType": "Mapreduce", "programId": "MapReduce2", "runtimeargs":{"arg1":"val1"}}, * {"appId": "App2", "programType": "Flow", "programId": "Flow1"}] * </code></pre> * </p><p> * The response will be an array of JsonObjects each of which will contain the three input parameters * as well as a "statusCode" field which maps to the status code for the data in that JsonObjects. * </p><p> * If an error occurs in the input (for the example above, App2 does not exist), then all JsonObjects for which the * parameters have a valid status will have the status field but all JsonObjects for which the parameters do not have * a valid status will have an error message and statusCode. * </p><p> * For example, if there is no App2 in the data above, then the response would be 200 OK with following possible data: * </p> * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1", "statusCode": 200}, * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 200}, * {"appId":"App2", "programType":"Flow", "programId":"Flow1", "statusCode":404, "error": "App: App2 not found"}] * </code></pre> */ @POST @Path("/start") public void startPrograms(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { List<BatchProgramStart> programs = validateAndGetBatchInput(request, BATCH_STARTS_TYPE); List<BatchProgramResult> output = new ArrayList<>(programs.size()); for (BatchProgramStart program : programs) { ProgramId programId = Ids.namespace(namespaceId).app(program.getAppId()).program(program.getProgramType(), program.getProgramId()); try { lifecycleService.start(programId, program.getRuntimeargs(), false); output.add(new BatchProgramResult(program, HttpResponseStatus.OK.getCode(), null)); } catch (NotFoundException e) { output.add(new BatchProgramResult(program, HttpResponseStatus.NOT_FOUND.getCode(), e.getMessage())); } catch (BadRequestException e) { output.add(new BatchProgramResult(program, HttpResponseStatus.BAD_REQUEST.getCode(), e.getMessage())); } catch (ConflictException e) { output.add(new BatchProgramResult(program, HttpResponseStatus.CONFLICT.getCode(), e.getMessage())); } } responder.sendJson(HttpResponseStatus.OK, output); } /** * Returns the number of instances for all program runnables that are passed into the data. The data is an array of * Json objects where each object must contain the following three elements: appId, programType, and programId * (flow name, service name). Retrieving instances only applies to flows, and user * services. For flows, another parameter, "runnableId", must be provided. This corresponds to the * flowlet/runnable for which to retrieve the instances. * <p> * Example input: * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1", "runnableId": "Runnable1"}, * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2"}, * {"appId": "App2", "programType": "Flow", "programId": "Flow1", "runnableId": "Flowlet1"}] * </code></pre> * </p><p> * The response will be an array of JsonObjects each of which will contain the three input parameters * as well as 3 fields: * <ul> * <li>"provisioned" which maps to the number of instances actually provided for the input runnable;</li> * <li>"requested" which maps to the number of instances the user has requested for the input runnable; and</li> * <li>"statusCode" which maps to the http status code for the data in that JsonObjects (200, 400, 404).</li> * </ul> * </p><p> * If an error occurs in the input (for the example above, Flowlet1 does not exist), then all JsonObjects for * which the parameters have a valid instances will have the provisioned and requested fields status code fields * but all JsonObjects for which the parameters are not valid will have an error message and statusCode. * </p><p> * For example, if there is no Flowlet1 in the above data, then the response could be 200 OK with the following data: * </p> * <pre><code> * [{"appId": "App1", "programType": "Service", "programId": "Service1", "runnableId": "Runnable1", * "statusCode": 200, "provisioned": 2, "requested": 2}, * {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2", "statusCode": 400, * "error": "Program type 'Mapreduce' is not a valid program type to get instances"}, * {"appId": "App2", "programType": "Flow", "programId": "Flow1", "runnableId": "Flowlet1", "statusCode": 404, * "error": "Program": Flowlet1 not found"}] * </code></pre> */ @POST @Path("/instances") public void getInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws IOException, BadRequestException { List<BatchRunnable> runnables = validateAndGetBatchInput(request, BATCH_RUNNABLES_TYPE); // cache app specs to perform fewer store lookups Map<Id.Application, ApplicationSpecification> appSpecs = new HashMap<>(); List<BatchRunnableInstances> output = new ArrayList<>(runnables.size()); for (BatchRunnable runnable : runnables) { // cant get instances for things that are not flows, services, or workers if (!canHaveInstances(runnable.getProgramType())) { output.add(new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.getCode(), String.format("Program type '%s' is not a valid program type to get instances", runnable.getProgramType().getPrettyName()))); continue; } Id.Application appId = Id.Application.from(namespaceId, runnable.getAppId()); // populate spec cache if this is the first time we've seen the appid. if (!appSpecs.containsKey(appId)) { appSpecs.put(appId, store.getApplication(appId)); } ApplicationSpecification spec = appSpecs.get(appId); if (spec == null) { output.add(new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(), String.format("App: %s not found", appId))); continue; } Id.Program programId = Id.Program.from(appId, runnable.getProgramType(), runnable.getProgramId()); output.add(getProgramInstances(runnable, spec, programId)); } responder.sendJson(HttpResponseStatus.OK, output); } /* Note: Cannot combine the following get all programs methods into one because then API path will clash with /apps path */ /** * Returns a list of flows associated with a namespace. */ @GET @Path("/flows") public void getAllFlows(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { programList(responder, namespaceId, ProgramType.FLOW, store); } /** * Returns a list of map/reduces associated with a namespace. */ @GET @Path("/mapreduce") public void getAllMapReduce(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { programList(responder, namespaceId, ProgramType.MAPREDUCE, store); } /** * Returns a list of spark jobs associated with a namespace. */ @GET @Path("/spark") public void getAllSpark(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { programList(responder, namespaceId, ProgramType.SPARK, store); } /** * Returns a list of workflows associated with a namespace. */ @GET @Path("/workflows") public void getAllWorkflows(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { programList(responder, namespaceId, ProgramType.WORKFLOW, store); } /** * Returns a list of services associated with a namespace. */ @GET @Path("/services") public void getAllServices(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { programList(responder, namespaceId, ProgramType.SERVICE, store); } @GET @Path("/workers") public void getAllWorkers(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { programList(responder, namespaceId, ProgramType.WORKER, store); } /** * Returns number of instances of a worker. */ @GET @Path("/apps/{app-id}/workers/{worker-id}/instances") public void getWorkerInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("worker-id") String workerId) { try { int count = store.getWorkerInstances(Id.Program.from(namespaceId, appId, ProgramType.WORKER, workerId)); responder.sendJson(HttpResponseStatus.OK, new Instances(count)); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } catch (Throwable e) { if (respondIfElementNotFound(e, responder)) { return; } throw e; } } /** * Sets the number of instances of a worker. */ @PUT @Path("/apps/{app-id}/workers/{worker-id}/instances") public void setWorkerInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("worker-id") String workerId) throws Exception { int instances = getInstances(request); try { lifecycleService.setInstances(Ids.namespace(namespaceId).app(appId).worker(workerId), instances); responder.sendStatus(HttpResponseStatus.OK); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } catch (Throwable e) { if (respondIfElementNotFound(e, responder)) { return; } throw e; } } /********************** Flow/Flowlet APIs ***********************************************************/ /** * Returns number of instances for a flowlet within a flow. */ @GET @Path("/apps/{app-id}/flows/{flow-id}/flowlets/{flowlet-id}/instances") public void getFlowletInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("flow-id") String flowId, @PathParam("flowlet-id") String flowletId) { try { int count = store.getFlowletInstances(Id.Program.from(namespaceId, appId, ProgramType.FLOW, flowId), flowletId); responder.sendJson(HttpResponseStatus.OK, new Instances(count)); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } catch (Throwable e) { if (respondIfElementNotFound(e, responder)) { return; } throw e; } } /** * Increases number of instance for a flowlet within a flow. */ @PUT @Path("/apps/{app-id}/flows/{flow-id}/flowlets/{flowlet-id}/instances") public synchronized void setFlowletInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("flow-id") String flowId, @PathParam("flowlet-id") String flowletId) throws Exception { int instances = getInstances(request); try { lifecycleService.setInstances(Ids.namespace(namespaceId).app(appId).flow(flowId), instances, flowletId); responder.sendStatus(HttpResponseStatus.OK); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } catch (Throwable e) { if (respondIfElementNotFound(e, responder)) { return; } throw e; } } @GET @Path("/apps/{app-id}/{program-category}/{program-id}/live-info") @SuppressWarnings("unused") public void liveInfo(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("program-category") String programCategory, @PathParam("program-id") String programId) { ProgramType type = getProgramType(programCategory); if (type == null) { responder.sendString(HttpResponseStatus.METHOD_NOT_ALLOWED, String.format("Live-info not supported for program type '%s'", programCategory)); return; } Id.Program program = Id.Program.from(namespaceId, appId, ProgramType.valueOfCategoryName(programCategory), programId); getLiveInfo(responder, program, runtimeService); } /** * Deletes queues. */ @DELETE @Path("/apps/{app-id}/flows/{flow-id}/queues") public void deleteFlowQueues(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("flow-id") String flowId) throws Exception { ProgramId programId = Ids.namespace(namespaceId).app(appId).flow(flowId); try { ProgramStatus status = lifecycleService.getProgramStatus(programId); if (ProgramStatus.RUNNING == status) { responder.sendString(HttpResponseStatus.FORBIDDEN, "Flow is running, please stop it first."); } else { queueAdmin.dropAllForFlow(Id.Flow.from(programId.getApplication(), programId.getProgram())); FlowUtils.deleteFlowPendingMetrics(metricStore, namespaceId, appId, flowId); responder.sendStatus(HttpResponseStatus.OK); } } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } } /** * Return the number of instances of a service. */ @GET @Path("/apps/{app-id}/services/{service-id}/instances") public void getServiceInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("service-id") String serviceId) { try { ProgramId programId = Ids.namespace(namespaceId).app(appId).service(serviceId); if (!store.programExists(programId.toId())) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Service not found"); return; } ServiceSpecification specification = (ServiceSpecification) lifecycleService.getProgramSpecification(programId); if (specification == null) { responder.sendStatus(HttpResponseStatus.NOT_FOUND); return; } int instances = specification.getInstances(); responder.sendJson(HttpResponseStatus.OK, new ServiceInstances(instances, getInstanceCount(programId, serviceId))); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } } /** * Set instances of a service. */ @PUT @Path("/apps/{app-id}/services/{service-id}/instances") public void setServiceInstances(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("app-id") String appId, @PathParam("service-id") String serviceId) throws Exception { try { ProgramId programId = Ids.namespace(namespaceId).app(appId).service(serviceId); if (!store.programExists(programId.toId())) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Service not found"); return; } int instances = getInstances(request); lifecycleService.setInstances(programId, instances); responder.sendStatus(HttpResponseStatus.OK); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } catch (Throwable throwable) { if (respondIfElementNotFound(throwable, responder)) { return; } throw throwable; } } @DELETE @Path("/queues") public synchronized void deleteQueues(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) { // synchronized to avoid a potential race condition here: // 1. the check for state returns that all flows are STOPPED // 2. The API deletes queues because // Between 1. and 2., a flow is started using the /namespaces/{namespace-id}/apps/{app-id}/flows/{flow-id}/start API // Averting this race condition by synchronizing this method. The resource that needs to be locked here is // runtimeService. This should work because the method that is used to start a flow - startStopProgram - is also // synchronized on this. // This synchronization works in HA mode because even in HA mode there is only one leader at a time. NamespaceId namespace = Ids.namespace(namespaceId); try { List<ProgramRecord> flows = listPrograms(namespace, ProgramType.FLOW, store); for (ProgramRecord flow : flows) { String appId = flow.getApp(); String flowId = flow.getName(); ProgramId programId = Ids.namespace(namespaceId).app(appId).flow(flowId); ProgramStatus status = lifecycleService.getProgramStatus(programId); if (ProgramStatus.STOPPED != status) { responder.sendString(HttpResponseStatus.FORBIDDEN, String.format("Flow '%s' from application '%s' in namespace '%s' is running, " + "please stop it first.", flowId, appId, namespaceId)); return; } } queueAdmin.dropAllInNamespace(namespace.toId()); // delete process metrics that are used to calculate the queue size (system.queue.pending metric) FlowUtils.deleteFlowPendingMetrics(metricStore, namespaceId, null, null); responder.sendStatus(HttpResponseStatus.OK); } catch (Exception e) { LOG.error("Error while deleting queues in namespace " + namespace, e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } /** * Get requested and provisioned instances for a program type. * The program type passed here should be one that can have instances (flows, services, ...) * Requires caller to do this validation. */ private BatchRunnableInstances getProgramInstances(BatchRunnable runnable, ApplicationSpecification spec, Id.Program programId) { int requested; String programName = programId.getId(); String runnableId = programName; ProgramType programType = programId.getType(); if (programType == ProgramType.WORKER) { if (!spec.getWorkers().containsKey(programName)) { return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(), "Worker: " + programName + " not found"); } requested = spec.getWorkers().get(programName).getInstances(); } else if (programType == ProgramType.SERVICE) { if (!spec.getServices().containsKey(programName)) { return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(), "Service: " + programName + " not found"); } requested = spec.getServices().get(programName).getInstances(); } else if (programType == ProgramType.FLOW) { // flows must have runnable id runnableId = runnable.getRunnableId(); if (runnableId == null) { return new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.getCode(), "Must provide the flowlet id as the runnableId for flows"); } FlowSpecification flowSpec = spec.getFlows().get(programName); if (flowSpec == null) { return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(), "Flow: " + programName + " not found"); } FlowletDefinition flowletDefinition = flowSpec.getFlowlets().get(runnableId); if (flowletDefinition == null) { return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.getCode(), "Flowlet: " + runnableId + " not found"); } requested = flowletDefinition.getInstances(); } else { return new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.getCode(), "Instances not supported for program type + " + programType); } int provisioned = getInstanceCount(programId.toEntityId(), runnableId); // use the pretty name of program types to be consistent return new BatchRunnableInstances(runnable, HttpResponseStatus.OK.getCode(), provisioned, requested); } private void getRuns(HttpResponder responder, Id.Program programId, String status, long start, long end, int limit) throws BadRequestException { try { ProgramRunStatus runStatus = (status == null) ? ProgramRunStatus.ALL : ProgramRunStatus.valueOf(status.toUpperCase()); List<RunRecord> records = Lists.transform(store.getRuns(programId, runStatus, start, end, limit), CONVERT_TO_RUN_RECORD); responder.sendJson(HttpResponseStatus.OK, records); } catch (IllegalArgumentException e) { throw new BadRequestException(String.format("Invalid status %s. Supported options for status of runs are " + "running/completed/failed", status)); } } /** * Returns the number of instances currently running for different runnables for different programs */ private int getInstanceCount(ProgramId programId, String runnableId) { ProgramLiveInfo info = runtimeService.getLiveInfo(programId.toId()); int count = 0; if (info instanceof NotRunningProgramLiveInfo) { return count; } if (info instanceof Containers) { Containers containers = (Containers) info; for (Containers.ContainerInfo container : containers.getContainers()) { if (container.getName().equals(runnableId)) { count++; } } return count; } // TODO: CDAP-1091: For standalone mode, returning the requested instances instead of provisioned only for services. // Doing this only for services to keep it consistent with the existing contract for flowlets right now. // The get instances contract for both flowlets and services should be re-thought and fixed as part of CDAP-1091 if (programId.getType() == ProgramType.SERVICE) { return getRequestedServiceInstances(programId.toId()); } // Not running on YARN default 1 return 1; } private int getRequestedServiceInstances(Id.Program serviceId) { // Not running on YARN, get it from store return store.getServiceInstances(serviceId); } private boolean isDebugAllowed(ProgramType programType) { return EnumSet.of(ProgramType.FLOW, ProgramType.SERVICE, ProgramType.WORKER).contains(programType); } private boolean canHaveInstances(ProgramType programType) { return EnumSet.of(ProgramType.FLOW, ProgramType.SERVICE, ProgramType.WORKER).contains(programType); } private <T extends BatchProgram> List<T> validateAndGetBatchInput(HttpRequest request, Type type) throws BadRequestException, IOException { List<T> programs; try (Reader reader = new InputStreamReader(new ChannelBufferInputStream(request.getContent()), Charsets.UTF_8)) { try { programs = GSON.fromJson(reader, type); if (programs == null) { throw new BadRequestException("Request body is invalid json, please check that it is a json array."); } } catch (JsonSyntaxException e) { throw new BadRequestException("Request body is invalid json: " + e.getMessage()); } } // validate input for (BatchProgram program : programs) { try { program.validate(); } catch (IllegalArgumentException e) { throw new BadRequestException( "Must provide valid appId, programType, and programId for each object: " + e.getMessage()); } } return programs; } }