From ee0968ad5167ff2ae0815fb25b010aa39e6cd4c8 Mon Sep 17 00:00:00 2001 From: Vaibhav Sethi Date: Mon, 27 Jan 2025 13:25:38 +0530 Subject: [PATCH] [CDAP-21096] Split ProgramLifecycleService for Appfabric service and processor Replace inheritance with composition --- .../guice/AppFabricServiceRuntimeModule.java | 28 +- .../AbstractProgramRuntimeService.java | 150 +++- .../app/runtime/ProgramRuntimeService.java | 66 +- .../java/io/cdap/cdap/app/store/Store.java | 11 + .../handlers/ProgramLifecycleHttpHandler.java | 720 +++--------------- .../handlers/ProgramRuntimeHttpHandler.java | 566 ++++++++++++++ .../handlers/util/NamespaceHelper.java | 56 ++ .../handlers/util/ProgramHandlerUtil.java | 119 +++ .../app/preview/DefaultPreviewRunner.java | 6 +- .../app/runtime/ProgramStartRequest.java | 53 ++ .../InMemoryProgramRuntimeService.java | 5 +- .../app/services/ProgramLifecycleService.java | 642 +++++----------- .../ProgramNotificationSubscriberService.java | 12 +- .../SystemProgramManagementService.java | 6 +- .../cdap/internal/app/store/DefaultStore.java | 13 + .../CapabilityManagementService.java | 4 +- .../AbstractProgramRuntimeServiceTest.java | 3 +- .../cdap/cdap/internal/AppFabricClient.java | 12 +- .../services/ProgramLifecycleServiceTest.java | 24 +- .../SystemProgramManagementServiceTest.java | 16 +- .../CapabilityManagementServiceTest.java | 23 +- .../cdap/gateway/router/RouterPathLookup.java | 70 +- .../gateway/router/RouterPathLookupTest.java | 45 +- .../RemoteProgramRunRecordFetcher.java | 2 +- 24 files changed, 1445 insertions(+), 1207 deletions(-) create mode 100644 cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramRuntimeHttpHandler.java create mode 100644 cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/NamespaceHelper.java create mode 100644 cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/ProgramHandlerUtil.java create mode 100644 cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramStartRequest.java diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/guice/AppFabricServiceRuntimeModule.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/guice/AppFabricServiceRuntimeModule.java index 3d4e18e861c5..50a89d181d50 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/guice/AppFabricServiceRuntimeModule.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/guice/AppFabricServiceRuntimeModule.java @@ -78,6 +78,7 @@ import io.cdap.cdap.gateway.handlers.ProfileHttpHandler; import io.cdap.cdap.gateway.handlers.ProgramLifecycleHttpHandler; import io.cdap.cdap.gateway.handlers.ProgramLifecycleHttpHandlerInternal; +import io.cdap.cdap.gateway.handlers.ProgramRuntimeHttpHandler; import io.cdap.cdap.gateway.handlers.ProvisionerHttpHandler; import io.cdap.cdap.gateway.handlers.SourceControlManagementHttpHandler; import io.cdap.cdap.gateway.handlers.TransactionHttpHandler; @@ -114,11 +115,11 @@ import io.cdap.cdap.internal.app.runtime.schedule.store.TriggerMisfireLogger; import io.cdap.cdap.internal.app.runtime.workflow.BasicWorkflowStateWriter; import io.cdap.cdap.internal.app.runtime.workflow.WorkflowStateWriter; +import io.cdap.cdap.internal.app.services.FlowControlService; import io.cdap.cdap.internal.app.services.LocalRunRecordCorrectorService; import io.cdap.cdap.internal.app.services.NoopRunRecordCorrectorService; import io.cdap.cdap.internal.app.services.ProgramLifecycleService; import io.cdap.cdap.internal.app.services.RunRecordCorrectorService; -import io.cdap.cdap.internal.app.services.FlowControlService; import io.cdap.cdap.internal.app.services.ScheduledRunRecordCorrectorService; import io.cdap.cdap.internal.app.store.DefaultStore; import io.cdap.cdap.internal.bootstrap.guice.BootstrapModules; @@ -249,17 +250,11 @@ protected void configure() { Names.named("appfabric.handler.hooks")); handlerHookNamesBinder.addBinding().toInstance(Constants.Service.APP_FABRIC_HTTP); - // TODO (CDAP-21112): Remove the addtional handler binding for in-memory and use the binding from - // AppFabricServiceModule, after fixing in-memory cache issue in ProgramRuntimeService and - // RunRecordMonitorService. + // For ProgramLifecycleHttpHandlerTest the ProgramRuntimeHttpHandler needs to be present + // in the appfabric service. Multibinder handlerBinder = Multibinder.newSetBinder( binder(), HttpHandler.class, Names.named(Constants.AppFabric.SERVER_HANDLERS_BINDING)); - handlerBinder.addBinding().to(BootstrapHttpHandler.class); - handlerBinder.addBinding().to(AppLifecycleHttpHandler.class); - handlerBinder.addBinding().to(AppLifecycleHttpHandlerInternal.class); - handlerBinder.addBinding().to(ProgramLifecycleHttpHandler.class); - handlerBinder.addBinding().to(ProgramLifecycleHttpHandlerInternal.class); - handlerBinder.addBinding().to(WorkflowHttpHandler.class); + handlerBinder.addBinding().to(ProgramRuntimeHttpHandler.class); // TODO: Uncomment after CDAP-7688 is resolved // servicesNamesBinder.addBinding().toInstance(Constants.Service.MESSAGING_SERVICE); @@ -529,6 +524,11 @@ protected void configure() { handlerBinder.addBinding().to(CredentialProviderHttpHandler.class); handlerBinder.addBinding().to(CredentialProviderHttpHandlerInternal.class); handlerBinder.addBinding().to(OperationHttpHandler.class); + handlerBinder.addBinding().to(AppLifecycleHttpHandler.class); + handlerBinder.addBinding().to(AppLifecycleHttpHandlerInternal.class); + handlerBinder.addBinding().to(ProgramLifecycleHttpHandler.class); + handlerBinder.addBinding().to(ProgramLifecycleHttpHandlerInternal.class); + handlerBinder.addBinding().to(WorkflowHttpHandler.class); FeatureFlagsProvider featureFlagsProvider = new DefaultFeatureFlagsProvider(cConf); if (Feature.NAMESPACED_SERVICE_ACCOUNTS.isEnabled(featureFlagsProvider)) { @@ -545,14 +545,8 @@ protected void configure() { Multibinder processorHandlerBinder = Multibinder.newSetBinder( binder(), HttpHandler.class, Names.named(AppFabric.PROCESSOR_HANDLERS_BINDING)); CommonHandlers.add(processorHandlerBinder); - // TODO (CDAP-21112): Move HTTP handler from Appfabric processor to server after fixing - // ProgramRuntimeService and RunRecordMonitorService. + processorHandlerBinder.addBinding().to(ProgramRuntimeHttpHandler.class); processorHandlerBinder.addBinding().to(BootstrapHttpHandler.class); - processorHandlerBinder.addBinding().to(AppLifecycleHttpHandler.class); - processorHandlerBinder.addBinding().to(AppLifecycleHttpHandlerInternal.class); - processorHandlerBinder.addBinding().to(ProgramLifecycleHttpHandler.class); - processorHandlerBinder.addBinding().to(ProgramLifecycleHttpHandlerInternal.class); - processorHandlerBinder.addBinding().to(WorkflowHttpHandler.class); } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeService.java index db1bb11f2dcc..d664d16171ff 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeService.java @@ -18,6 +18,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Table; import com.google.common.io.Closeables; @@ -26,12 +27,14 @@ import com.google.inject.Inject; import io.cdap.cdap.app.deploy.ProgramRunDispatcherContext; import io.cdap.cdap.app.program.ProgramDescriptor; +import io.cdap.cdap.common.BadRequestException; import io.cdap.cdap.common.app.RunIds; import io.cdap.cdap.common.conf.CConfiguration; import io.cdap.cdap.common.conf.Constants; import io.cdap.cdap.common.twill.TwillAppNames; import io.cdap.cdap.internal.app.deploy.ProgramRunDispatcherFactory; import io.cdap.cdap.internal.app.runtime.AbstractListener; +import io.cdap.cdap.internal.app.runtime.ProgramOptionConstants; import io.cdap.cdap.internal.app.runtime.service.SimpleRuntimeInfo; import io.cdap.cdap.proto.InMemoryProgramLiveInfo; import io.cdap.cdap.proto.NotRunningProgramLiveInfo; @@ -46,6 +49,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -59,6 +64,7 @@ import org.apache.twill.api.TwillController; import org.apache.twill.api.TwillRunner; import org.apache.twill.api.TwillRunnerService; +import org.apache.twill.api.logging.LogEntry; import org.apache.twill.common.Threads; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,27 +127,38 @@ void setRemoteTwillRunnerService( @Override public final RuntimeInfo run(ProgramDescriptor programDescriptor, ProgramOptions options, RunId runId) { - ProgramRunDispatcherContext dispatcherContext = new ProgramRunDispatcherContext( - programDescriptor, options, runId, - isDistributed()); ProgramId programId = programDescriptor.getProgramId(); ProgramRunId programRunId = programId.run(runId); - DelayedProgramController controller = new DelayedProgramController(programRunId); - RuntimeInfo runtimeInfo = createRuntimeInfo(controller, programId, - dispatcherContext::executeCleanupTasks); - updateRuntimeInfo(runtimeInfo); - executor.execute(() -> { - try { - controller.setProgramController( - programRunDispatcherFactory.getProgramRunDispatcher(programId.getType()) - .dispatchProgram(dispatcherContext)); - } catch (Exception e) { - controller.failed(e); - programStateWriter.error(programRunId, e); - LOG.error("Exception while trying to run program run {}", programRunId, e); + Lock lock = runtimeInfosLock.writeLock(); + lock.lock(); + try { + RuntimeInfo runtimeInfo = lookup(programRunId.getParent(), runId); + if (runtimeInfo != null) { + return runtimeInfo; } - }); - return runtimeInfo; + + ProgramRunDispatcherContext dispatcherContext = new ProgramRunDispatcherContext( + programDescriptor, options, runId, + isDistributed()); + DelayedProgramController controller = new DelayedProgramController(programRunId); + runtimeInfo = createRuntimeInfo(controller, programId, + dispatcherContext::executeCleanupTasks); + updateRuntimeInfo(runtimeInfo); + executor.execute(() -> { + try { + controller.setProgramController( + programRunDispatcherFactory.getProgramRunDispatcher(programId.getType()) + .dispatchProgram(dispatcherContext)); + } catch (Exception e) { + controller.failed(e); + programStateWriter.error(programRunId, e); + LOG.error("Exception while trying to run program run {}", programRunId, e); + } + }); + return runtimeInfo; + } finally { + lock.unlock(); + } } @Override @@ -239,6 +256,28 @@ public List listAll(ProgramType... types) { return runningPrograms; } + @Override + public void resetProgramLogLevels(ProgramId programId, Set loggerNames, + @Nullable String runId) throws Exception { + if (!EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programId.getType())) { + throw new BadRequestException( + String.format("Resetting log levels for program type %s is not supported", + programId.getType().getPrettyName())); + } + resetLogLevels(programId, loggerNames, runId); + } + + @Override + public void updateProgramLogLevels(ProgramId programId, Map logLevels, + @Nullable String runId) throws Exception { + if (!EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programId.getType())) { + throw new BadRequestException( + String.format("Updating log levels for program type %s is not supported", + programId.getType().getPrettyName())); + } + updateLogLevels(programId, logLevels, runId); + } + @Override protected void startUp() throws Exception { // Limits to at max poolSize number of concurrent program launch. @@ -436,4 +475,79 @@ private void cleanupRuntimeInfo(@Nullable RuntimeInfo info) { Closeables.closeQuietly((Closeable) info); } } + + /** + * Helper method to get the {@link LogLevelUpdater} for the program. + */ + private LogLevelUpdater getLogLevelUpdater(RuntimeInfo runtimeInfo) throws Exception { + ProgramController programController = runtimeInfo.getController(); + if (!(programController instanceof LogLevelUpdater)) { + throw new BadRequestException( + "Update log levels at runtime is only supported in distributed mode"); + } + return ((LogLevelUpdater) programController); + } + + /** + * Helper method to update log levels for Worker or Service. + */ + private void updateLogLevels(ProgramId programId, Map logLevels, + @Nullable String runId) throws Exception { + ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId, runId).values() + .stream() + .findFirst().orElse(null); + if (runtimeInfo != null) { + LogLevelUpdater logLevelUpdater = getLogLevelUpdater(runtimeInfo); + logLevelUpdater.updateLogLevels(logLevels, null); + } + } + + /** + * Helper method to reset log levels for Worker or Service. + */ + private void resetLogLevels(ProgramId programId, Set loggerNames, @Nullable String runId) + throws Exception { + ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId, runId).values() + .stream() + .findFirst().orElse(null); + if (runtimeInfo != null) { + LogLevelUpdater logLevelUpdater = getLogLevelUpdater(runtimeInfo); + logLevelUpdater.resetLogLevels(loggerNames, null); + } + } + + @Override + public void setInstances(ProgramId programId, int instances, int oldInstances) + throws ExecutionException, InterruptedException, BadRequestException { + ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId); + if (runtimeInfo != null) { + runtimeInfo.getController().command(ProgramOptionConstants.INSTANCES, + ImmutableMap.of("runnable", programId.getProgram(), + "newInstances", String.valueOf(instances), + "oldInstances", String.valueOf(oldInstances))).get(); + } + } + + private Map findRuntimeInfo( + ProgramId programId, @Nullable String runId) throws BadRequestException { + + if (runId != null) { + RunId run; + try { + run = RunIds.fromString(runId); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Error parsing run-id.", e); + } + ProgramRuntimeService.RuntimeInfo runtimeInfo = lookup(programId, run); + return runtimeInfo == null ? Collections.emptyMap() + : Collections.singletonMap(run, runtimeInfo); + } + return new HashMap<>(list(programId)); + } + + @Nullable + protected ProgramRuntimeService.RuntimeInfo findRuntimeInfo(ProgramId programId) + throws BadRequestException { + return findRuntimeInfo(programId, null).values().stream().findFirst().orElse(null); + } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/ProgramRuntimeService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/ProgramRuntimeService.java index 67bcf04ae730..df9f321d854d 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/ProgramRuntimeService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/runtime/ProgramRuntimeService.java @@ -18,13 +18,19 @@ import com.google.common.util.concurrent.Service; import io.cdap.cdap.app.program.ProgramDescriptor; +import io.cdap.cdap.common.BadRequestException; import io.cdap.cdap.proto.ProgramLiveInfo; import io.cdap.cdap.proto.ProgramType; import io.cdap.cdap.proto.id.ProgramId; +import io.cdap.cdap.proto.security.StandardPermission; +import io.cdap.cdap.security.spi.authorization.UnauthorizedException; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; import org.apache.twill.api.RunId; +import org.apache.twill.api.logging.LogEntry; /** * Service for interacting with the runtime system. @@ -47,7 +53,9 @@ interface RuntimeInfo { } /** - * Starts the given program and return a {@link RuntimeInfo} about the running program. + * Runs the given program and return a {@link RuntimeInfo} about the running program. The program + * is run if it is not already running, otherwise the {@link RuntimeInfo} of the already running + * program is returned. * * @param programDescriptor describing the program to run * @param options {@link ProgramOptions} that are needed by the program. @@ -97,4 +105,60 @@ interface RuntimeInfo { * @param types Types of program to check returns List of info about running programs. */ List listAll(ProgramType... types); + + /** + * Reset log levels for the given program. Only supported program types for this action are {@link + * ProgramType#SERVICE} and {@link ProgramType#WORKER}. + * + * @param programId the {@link ProgramId} of the program for which log levels are to be + * reset. + * @param loggerNames the {@link String} set of the logger names to be updated, empty means + * reset for all loggers. + * @param runId the run id of the program. + * @throws InterruptedException if there is an error while asynchronously resetting log + * levels. + * @throws ExecutionException if there is an error while asynchronously resetting log levels. + * @throws UnauthorizedException if the user does not have privileges to reset log levels for + * the specified program. To reset log levels for a program, a user needs {@link + * StandardPermission#UPDATE} on the program. + */ + void resetProgramLogLevels(ProgramId programId, Set loggerNames, @Nullable String runId) throws Exception; + + /** + * Update log levels for the given program. Only supported program types for this action are + * {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. + * + * @param programId the {@link ProgramId} of the program for which log levels are to be + * updated + * @param logLevels the {@link Map} of the log levels to be updated. + * @param runId the run id of the program. + * @throws InterruptedException if there is an error while asynchronously updating log + * levels. + * @throws ExecutionException if there is an error while asynchronously updating log levels. + * @throws BadRequestException if the log level is not valid or the program type is not + * supported. + * @throws UnauthorizedException if the user does not have privileges to update log levels for + * the specified program. To update log levels for a program, a user needs {@link + * StandardPermission#UPDATE} on the program. + */ + void updateProgramLogLevels(ProgramId programId, Map logLevels, @Nullable String runId) + throws Exception; + + /** + * Set instances for the given program. Only supported program types for this action are {@link + * ProgramType#SERVICE} and {@link ProgramType#WORKER}. + * + * @param programId the {@link ProgramId} of the program for which instances are to be + * updated + * @param instances the number of instances to be updated. + * @param instances the previous number of instances. + * + * @throws InterruptedException if there is an error while asynchronously updating instances + * @throws ExecutionException if there is an error while asynchronously updating instances + * @throws BadRequestException if the number of instances specified is less than 0 + * @throws UnauthorizedException if the user does not have privileges to set instances for the + * specified program. To set instances for a program, a user needs {@link + * StandardPermission#UPDATE} on the program. + */ + void setInstances(ProgramId programId, int instances, int oldInstances) throws Exception; } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java index 6463e243fdc9..c47e42880aaa 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/app/store/Store.java @@ -26,6 +26,7 @@ import io.cdap.cdap.app.program.Program; import io.cdap.cdap.app.program.ProgramDescriptor; import io.cdap.cdap.common.ApplicationNotFoundException; +import io.cdap.cdap.common.BadRequestException; import io.cdap.cdap.common.ConflictException; import io.cdap.cdap.common.NotFoundException; import io.cdap.cdap.common.ProgramNotFoundException; @@ -464,6 +465,16 @@ void updateApplicationSourceControlMeta(Map up @Nullable ApplicationMeta getLatest(ApplicationReference appRef); + /** + * Gets the ApplicationId with the latest version for the given ApplicationReference. + * + * @param appRef ApplicationReference + * @return ApplicationId for the latest version + * + * @throws ApplicationNotFoundException if the app was not found for the given application reference. + */ + ApplicationId getLatestApp(ApplicationReference appRef) throws ApplicationNotFoundException; + /** * Scans for the latest applications across all namespaces. * diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramLifecycleHttpHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramLifecycleHttpHandler.java index 888bad896edf..d20df72fb68d 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramLifecycleHttpHandler.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramLifecycleHttpHandler.java @@ -19,7 +19,6 @@ import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Objects; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -31,38 +30,31 @@ import com.google.inject.Singleton; import io.cdap.cdap.api.ProgramSpecification; import io.cdap.cdap.api.app.ApplicationSpecification; -import io.cdap.cdap.api.schedule.Trigger; +import io.cdap.cdap.api.service.ServiceUnavailableException; import io.cdap.cdap.app.mapreduce.MRJobInfoFetcher; -import io.cdap.cdap.app.runtime.ProgramRuntimeService; import io.cdap.cdap.app.store.Store; -import io.cdap.cdap.common.ApplicationNotFoundException; import io.cdap.cdap.common.BadRequestException; import io.cdap.cdap.common.ConflictException; -import io.cdap.cdap.common.NamespaceNotFoundException; import io.cdap.cdap.common.NotFoundException; import io.cdap.cdap.common.NotImplementedException; -import io.cdap.cdap.api.service.ServiceUnavailableException; import io.cdap.cdap.common.app.RunIds; import io.cdap.cdap.common.conf.Constants; import io.cdap.cdap.common.discovery.EndpointStrategy; import io.cdap.cdap.common.discovery.RandomEndpointStrategy; import io.cdap.cdap.common.id.Id; -import io.cdap.cdap.common.io.CaseInsensitiveEnumTypeAdapterFactory; import io.cdap.cdap.common.namespace.NamespaceQueryAdmin; import io.cdap.cdap.common.security.AuditDetail; import io.cdap.cdap.common.security.AuditPolicy; import io.cdap.cdap.common.service.ServiceDiscoverable; import io.cdap.cdap.gateway.handlers.util.AbstractAppFabricHttpHandler; -import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter; +import io.cdap.cdap.gateway.handlers.util.NamespaceHelper; +import io.cdap.cdap.gateway.handlers.util.ProgramHandlerUtil; import io.cdap.cdap.internal.app.runtime.schedule.ProgramSchedule; import io.cdap.cdap.internal.app.runtime.schedule.ProgramScheduleRecord; import io.cdap.cdap.internal.app.runtime.schedule.ProgramScheduleStatus; import io.cdap.cdap.internal.app.runtime.schedule.SchedulerException; -import io.cdap.cdap.internal.app.runtime.schedule.constraint.ConstraintCodec; import io.cdap.cdap.internal.app.runtime.schedule.store.Schedulers; import io.cdap.cdap.internal.app.runtime.schedule.trigger.ProgramStatusTrigger; -import io.cdap.cdap.internal.app.runtime.schedule.trigger.SatisfiableTrigger; -import io.cdap.cdap.internal.app.runtime.schedule.trigger.TriggerCodec; import io.cdap.cdap.internal.app.services.ProgramLifecycleService; import io.cdap.cdap.internal.app.store.ApplicationMeta; import io.cdap.cdap.internal.app.store.RunRecordDetail; @@ -74,14 +66,8 @@ import io.cdap.cdap.proto.BatchProgramSchedule; import io.cdap.cdap.proto.BatchProgramStart; import io.cdap.cdap.proto.BatchProgramStatus; -import io.cdap.cdap.proto.BatchRunnable; -import io.cdap.cdap.proto.BatchRunnableInstances; -import io.cdap.cdap.proto.Containers; -import io.cdap.cdap.proto.Instances; import io.cdap.cdap.proto.MRJobInfo; -import io.cdap.cdap.proto.NotRunningProgramLiveInfo; import io.cdap.cdap.proto.ProgramHistory; -import io.cdap.cdap.proto.ProgramLiveInfo; import io.cdap.cdap.proto.ProgramRunStatus; import io.cdap.cdap.proto.ProgramStatus; import io.cdap.cdap.proto.ProgramType; @@ -90,7 +76,6 @@ import io.cdap.cdap.proto.RunRecord; import io.cdap.cdap.proto.ScheduleDetail; import io.cdap.cdap.proto.ScheduledRuntime; -import io.cdap.cdap.proto.ServiceInstances; import io.cdap.cdap.proto.id.ApplicationId; import io.cdap.cdap.proto.id.ApplicationReference; import io.cdap.cdap.proto.id.NamespaceId; @@ -109,12 +94,10 @@ import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -146,8 +129,6 @@ public class ProgramLifecycleHttpHandler extends AbstractAppFabricHttpHandler { private static final Logger LOG = LoggerFactory.getLogger(ProgramLifecycleHttpHandler.class); private static final Type BATCH_PROGRAMS_TYPE = new TypeToken>() { }.getType(); - private static final Type BATCH_RUNNABLES_TYPE = new TypeToken>() { - }.getType(); private static final Type BATCH_STARTS_TYPE = new TypeToken>() { }.getType(); @@ -155,52 +136,21 @@ public class ProgramLifecycleHttpHandler extends AbstractAppFabricHttpHandler { private static final String SCHEDULES = "schedules"; - /** - * Json serializer/deserializer. - */ - private static final Gson GSON = ApplicationSpecificationAdapter - .addTypeAdapters(new GsonBuilder()) - .registerTypeAdapter(Trigger.class, new TriggerCodec()) - .registerTypeAdapter(SatisfiableTrigger.class, new TriggerCodec()) - .registerTypeAdapter(Constraint.class, new ConstraintCodec()) - .create(); - - /** - * Json serde for decoding request. It uses a case insensitive enum adapter. - */ - private static final Gson DECODE_GSON = ApplicationSpecificationAdapter - .addTypeAdapters(new GsonBuilder()) - .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()) - .registerTypeAdapter(Trigger.class, new TriggerCodec()) - .registerTypeAdapter(SatisfiableTrigger.class, new TriggerCodec()) - .registerTypeAdapter(Constraint.class, new ConstraintCodec()) - .create(); - private final ProgramScheduleService programScheduleService; private final ProgramLifecycleService lifecycleService; private final DiscoveryServiceClient discoveryServiceClient; private final MRJobInfoFetcher mrJobInfoFetcher; private final NamespaceQueryAdmin namespaceQueryAdmin; - - /** - * Store manages non-runtime lifecycle. - */ private final Store store; - /** - * Runtime program service for running and managing programs. - */ - private final ProgramRuntimeService runtimeService; - @Inject - ProgramLifecycleHttpHandler(Store store, ProgramRuntimeService runtimeService, + ProgramLifecycleHttpHandler(Store store, DiscoveryServiceClient discoveryServiceClient, ProgramLifecycleService lifecycleService, MRJobInfoFetcher mrJobInfoFetcher, NamespaceQueryAdmin namespaceQueryAdmin, ProgramScheduleService programScheduleService) { this.store = store; - this.runtimeService = runtimeService; this.discoveryServiceClient = discoveryServiceClient; this.lifecycleService = lifecycleService; this.mrJobInfoFetcher = mrJobInfoFetcher; @@ -300,7 +250,7 @@ public void getStatusVersioned(HttpRequest request, HttpResponder responder, return; } - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramStatus programStatus; if (ApplicationId.DEFAULT_VERSION.equals(versionId)) { programStatus = lifecycleService.getProgramStatus( @@ -311,7 +261,7 @@ public void getStatusVersioned(HttpRequest request, HttpResponder responder, } Map status = ImmutableMap.of("status", programStatus.name()); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(status)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(status)); } /** @@ -325,7 +275,7 @@ public void performRunLevelStop(HttpRequest request, HttpResponder responder, @PathParam("program-type") String type, @PathParam("program-id") String programId, @PathParam("run-id") String runId) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramReference program = new ApplicationReference(namespaceId, appId).program(programType, programId); @@ -408,7 +358,7 @@ private void doPerformAction(FullHttpRequest request, HttpResponder responder, S return; } - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramReference programReference = new ProgramReference(namespaceId, appId, programType, programId); ProgramId program = applicationId.program(programType, programId); @@ -491,7 +441,7 @@ public void programHistory(HttpRequest request, HttpResponder responder, @QueryParam("end") String endTs, @QueryParam("limit") @DefaultValue("100") final int resultLimit) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); long start = parseAndValidate(startTs, 0L, "start"); long end = parseAndValidate(endTs, Long.MAX_VALUE, "end"); @@ -508,7 +458,7 @@ public void programHistory(HttpRequest request, HttpResponder responder, List records = lifecycleService.getAllRunRecords(programReference, runStatus, start, end, resultLimit, record -> !isTetheredRunRecord(record)); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(records)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(records)); } /** @@ -528,7 +478,7 @@ public void programHistoryVersioned(HttpRequest request, HttpResponder responder @QueryParam("end") String endTs, @QueryParam("limit") @DefaultValue("100") final int resultLimit) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); long start = parseAndValidate(startTs, 0L, "start"); long end = parseAndValidate(endTs, Long.MAX_VALUE, "end"); @@ -553,7 +503,7 @@ public void programHistoryVersioned(HttpRequest request, HttpResponder responder } records = records.stream().filter(record -> !isTetheredRunRecord(record)) .collect(Collectors.toList()); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(records)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(records)); } private long parseAndValidate(String strVal, long defaultVal, String paramName) @@ -578,12 +528,19 @@ public void programRunRecord(HttpRequest request, HttpResponder responder, @PathParam("app-name") String appName, @PathParam("program-type") String type, @PathParam("program-name") String programName, - @PathParam("run-id") String runid) throws NotFoundException, BadRequestException { - RunRecordDetail runRecordMeta = getRunRecordDetailFromId(namespaceId, appName, type, - programName, runid); + @PathParam("run-id") String runId) throws NotFoundException, BadRequestException { + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + ProgramReference programRef = new ApplicationReference(namespaceId, appName).program(programType, + programName); + RunRecordDetail runRecordMeta = store.getRun(programRef, runId); + if (runRecordMeta == null) { + throw new NotFoundException( + String.format("No run record found for program %s and runID: %s", programRef, runId)); + } + if (!isTetheredRunRecord(runRecordMeta)) { RunRecord runRecord = RunRecord.builder(runRecordMeta).build(); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(runRecord)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(runRecord)); return; } throw new NotFoundException(runRecordMeta.getProgramRunId()); @@ -605,13 +562,13 @@ public void programRunRecordVersioned(HttpRequest request, @PathParam("program-name") String programName, @PathParam("run-id") String runid) throws NotFoundException, BadRequestException { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramId progId = new ApplicationId(namespaceId, appName, appVersion).program(programType, programName); RunRecordDetail runRecordMeta = store.getRun(progId.run(runid)); if (runRecordMeta != null && !isTetheredRunRecord(runRecordMeta)) { RunRecord runRecord = RunRecord.builder(runRecordMeta).build(); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(runRecord)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(runRecord)); return; } throw new NotFoundException(progId.run(runid)); @@ -627,10 +584,9 @@ public void getProgramRuntimeArgs(HttpRequest request, HttpResponder responder, @PathParam("app-name") String appName, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - ProgramType programType = getProgramType(type); - String appVersion = getLatestAppVersion(new NamespaceId(namespaceId), appName); - ProgramId programId = new ApplicationId(namespaceId, appName, appVersion).program(programType, - programName); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + ProgramId programId = store.getLatestApp(new ApplicationReference(namespaceId, appName)) + .program(programType, programName); getProgramIdRuntimeArgs(programId, responder); } @@ -645,9 +601,8 @@ public void saveProgramRuntimeArgs(FullHttpRequest request, HttpResponder respon @PathParam("app-name") String appName, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - ProgramType programType = getProgramType(type); - String appVersion = getLatestAppVersion(new NamespaceId(namespaceId), appName); - ProgramId programId = new ApplicationId(namespaceId, appName, appVersion).program(programType, + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + ProgramId programId = store.getLatestApp(new ApplicationReference(namespaceId, appName)).program(programType, programName); saveProgramIdRuntimeArgs(programId, request, responder); } @@ -666,7 +621,7 @@ public void getProgramRuntimeArgsVersioned(HttpRequest request, HttpResponder re @PathParam("app-version") String appVersion, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramId programId = new ApplicationId(namespaceId, appName, appVersion).program(programType, programName); getProgramIdRuntimeArgs(programId, responder); @@ -687,12 +642,12 @@ public void saveProgramRuntimeArgsVersioned(FullHttpRequest request, HttpRespond @PathParam("app-version") String appVersion, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - String latestVersion = getLatestAppVersion(new NamespaceId(namespaceId), appName); + String latestVersion = store.getLatestApp(new ApplicationReference(namespaceId, appName)).getVersion(); if (!appVersion.equals(latestVersion) && !ApplicationId.DEFAULT_VERSION.equals(appVersion)) { throw new BadRequestException( "Runtime arguments can only be changed on the latest program version"); } - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramId programId = new ApplicationId(namespaceId, appName, appVersion).program(programType, programName); saveProgramIdRuntimeArgs(programId, request, responder); @@ -701,7 +656,7 @@ public void saveProgramRuntimeArgsVersioned(FullHttpRequest request, HttpRespond private void getProgramIdRuntimeArgs(ProgramId programId, HttpResponder responder) throws Exception { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(lifecycleService.getRuntimeArgs(programId))); + ProgramHandlerUtil.toJson(lifecycleService.getRuntimeArgs(programId))); } private void saveProgramIdRuntimeArgs(ProgramId programId, FullHttpRequest request, @@ -733,7 +688,7 @@ public void programSpecificationVersioned(HttpRequest request, HttpResponder res @PathParam("app-version") String appVersion, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ApplicationId application = new ApplicationId(namespaceId, appName, appVersion); ProgramId programId = application.program(programType, programName); ProgramSpecification specification; @@ -747,7 +702,7 @@ public void programSpecificationVersioned(HttpRequest request, HttpResponder res if (specification == null) { throw new NotFoundException(programId); } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(specification)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(specification)); } /** @@ -812,7 +767,7 @@ public void getProgramStatusSchedules(HttpRequest request, HttpResponder respond throw new BadRequestException("Must specify trigger-program-name as a query param"); } - ProgramType programType = getProgramType(triggerProgramType); + ProgramType programType = ProgramType.valueOfCategoryName(triggerProgramType, BadRequestException::new); ProgramScheduleStatus programScheduleStatus; try { programScheduleStatus = @@ -853,7 +808,7 @@ public void getProgramStatusSchedules(HttpRequest request, HttpResponder respond .map(ProgramScheduleRecord::toScheduleDetail) .collect(Collectors.toList()); responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(details, Schedulers.SCHEDULE_DETAILS_TYPE)); + ProgramHandlerUtil.toJson(details, Schedulers.SCHEDULE_DETAILS_TYPE)); } @GET @@ -885,7 +840,7 @@ private void doGetSchedule(HttpResponder responder, String namespace, String app ScheduleId scheduleId = new ApplicationId(namespace, app).schedule(scheduleName); ProgramScheduleRecord record = programScheduleService.getRecord(scheduleId); ScheduleDetail detail = record.toScheduleDetail(); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(detail, ScheduleDetail.class)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(detail, ScheduleDetail.class)); } /** @@ -963,7 +918,7 @@ public void getProgramSchedulesVersioned(HttpRequest request, HttpResponder resp @PathParam("program-name") String program, @QueryParam("trigger-type") String triggerType, @QueryParam("schedule-status") String scheduleStatus) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); if (programType.getSchedulableType() == null) { throw new BadRequestException("Program type " + programType + " cannot have schedule"); } @@ -1024,7 +979,7 @@ record -> record.getSchedule().getTrigger().getType().equals(triggerType)); .map(ProgramScheduleRecord::toScheduleDetail) .collect(Collectors.toList()); responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(details, Schedulers.SCHEDULE_DETAILS_TYPE)); + ProgramHandlerUtil.toJson(details, Schedulers.SCHEDULE_DETAILS_TYPE)); } /** @@ -1034,14 +989,12 @@ record -> record.getSchedule().getTrigger().getType().equals(triggerType)); @Path("/apps/{app-name}/{program-type}/{program-name}/previousruntime") public void getPreviousScheduledRunTime(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, - @PathParam("app-name") String appId, + @PathParam("app-name") String appName, @PathParam("program-type") String type, @PathParam("program-name") String program) throws Exception { - ProgramType programType = getProgramType(type); - String appVersion = getLatestAppVersion(new NamespaceId(namespaceId), appId); - handleScheduleRunTime(responder, - new NamespaceId(namespaceId).app(appId, appVersion).program(programType, program), - true); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + ApplicationId appId = store.getLatestApp(new ApplicationReference(namespaceId, appName)); + handleScheduleRunTime(responder, appId.program(programType, program), true); } /** @@ -1051,14 +1004,12 @@ public void getPreviousScheduledRunTime(HttpRequest request, HttpResponder respo @Path("/apps/{app-name}/{program-type}/{program-name}/nextruntime") public void getNextScheduledRunTime(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, - @PathParam("app-name") String appId, + @PathParam("app-name") String appName, @PathParam("program-type") String type, @PathParam("program-name") String program) throws Exception { - ProgramType programType = getProgramType(type); - String appVersion = getLatestAppVersion(new NamespaceId(namespaceId), appId); - handleScheduleRunTime(responder, - new NamespaceId(namespaceId).app(appId, appVersion).program(programType, program), - false); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + ApplicationId appId = store.getLatestApp(new ApplicationReference(namespaceId, appName)); + handleScheduleRunTime(responder, appId.program(programType, program), false); } private void handleScheduleRunTime(HttpResponder responder, ProgramId programId, @@ -1066,7 +1017,7 @@ private void handleScheduleRunTime(HttpResponder responder, ProgramId programId, try { lifecycleService.ensureProgramExists(programId); responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(getScheduledRunTimes(programId, previousRuntimeRequested))); + ProgramHandlerUtil.toJson(getScheduledRunTimes(programId, previousRuntimeRequested))); } catch (SecurityException e) { responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); } @@ -1096,9 +1047,9 @@ private void handleScheduleRunTime(HttpResponder responder, ProgramId programId, public void batchPreviousRunTimes(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List batchPrograms = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); + List batchPrograms = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(batchRunTimes(namespaceId, batchPrograms, true))); + ProgramHandlerUtil.toJson(batchRunTimes(namespaceId, batchPrograms, true))); } /** @@ -1125,9 +1076,9 @@ public void batchPreviousRunTimes(FullHttpRequest request, public void batchNextRunTimes(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List batchPrograms = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); + List batchPrograms = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(batchRunTimes(namespaceId, batchPrograms, false))); + ProgramHandlerUtil.toJson(batchRunTimes(namespaceId, batchPrograms, false))); } /** @@ -1297,7 +1248,7 @@ private ScheduleDetail readScheduleDetailBody(FullHttpRequest request, String sc try (Reader reader = new InputStreamReader(new ByteBufInputStream(request.content()), Charsets.UTF_8)) { // The schedule spec in the request body does not contain the program information - json = DECODE_GSON.fromJson(reader, JsonElement.class); + json = ProgramHandlerUtil.fromJson(reader, JsonElement.class); } catch (IOException e) { throw new IOException("Error reading request body", e); } catch (JsonSyntaxException e) { @@ -1305,11 +1256,11 @@ private ScheduleDetail readScheduleDetailBody(FullHttpRequest request, String sc } if (!json.isJsonObject()) { throw new BadRequestException( - "Expected a json object in the request body but received " + GSON.toJson(json)); + "Expected a json object in the request body but received " + ProgramHandlerUtil.toJson(json)); } ScheduleDetail scheduleDetail; try { - scheduleDetail = DECODE_GSON.fromJson(json, ScheduleDetail.class); + scheduleDetail = ProgramHandlerUtil.fromJson(json, ScheduleDetail.class); } catch (JsonSyntaxException e) { throw new BadRequestException( "Error parsing request body as a schedule specification: " + e.getMessage()); @@ -1354,107 +1305,6 @@ private void doDeleteSchedule(HttpResponder responder, String namespaceId, Strin responder.sendStatus(HttpResponseStatus.OK); } - /** - * Update the log level for a running program according to the request body. Currently supported - * program types are {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. The request body - * is expected to contain a map of log levels, where key is loggername, value is one of the valid - * {@link org.apache.twill.api.logging.LogEntry.Level} or null. - * - */ - @PUT - @Path("/apps/{app-name}/{program-type}/{program-name}/runs/{run-id}/loglevels") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void updateProgramLogLevels(FullHttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespace, - @PathParam("app-name") String appName, - @PathParam("program-type") String type, - @PathParam("program-name") String programName, - @PathParam("run-id") String runId) throws Exception { - RunRecordDetail run = getRunRecordDetailFromId(namespace, appName, type, programName, runId); - updateLogLevels(request, responder, namespace, appName, run.getVersion(), type, programName, - runId); - } - - /** - * Update the log level for a running program according to the request body. - * Deprecated : Run-id is sufficient to identify a program run. - */ - @Deprecated - @PUT - @Path("/apps/{app-name}/versions/{app-version}/{program-type}/{program-name}/runs/{run-id}/loglevels") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void updateProgramLogLevelsVersioned(FullHttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespace, - @PathParam("app-name") String appName, - @PathParam("app-version") String appVersion, - @PathParam("program-type") String type, - @PathParam("program-name") String programName, - @PathParam("run-id") String runId) throws Exception { - updateLogLevels(request, responder, namespace, appName, appVersion, type, programName, runId); - } - - /** - * Reset the log level for a running program back to where it starts. Currently supported program - * types are {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. The request body can - * either be empty, which will reset all loggers for the program, or contain a list of logger - * names, which will reset for these particular logger names for the program. - */ - @POST - @Path("/apps/{app-name}/{program-type}/{program-name}/runs/{run-id}/resetloglevels") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void resetProgramLogLevels(FullHttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespace, - @PathParam("app-name") String appName, - @PathParam("program-type") String type, - @PathParam("program-name") String programName, - @PathParam("run-id") String runId) throws Exception { - RunRecordDetail run = getRunRecordDetailFromId(namespace, appName, type, programName, runId); - resetLogLevels(request, responder, namespace, appName, run.getVersion(), type, programName, - runId); - } - - /** - * Reset the log level for a running program back to where it starts. - * - * Deprecated : Run-id is sufficient to identify a program run. - */ - @POST - @Path("/apps/{app-name}/versions/{app-version}/{program-type}/{program-name}/runs/{run-id}/resetloglevels") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void resetProgramLogLevelsVersioned(FullHttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespace, - @PathParam("app-name") String appName, - @PathParam("app-version") String appVersion, - @PathParam("program-type") String type, - @PathParam("program-name") String programName, - @PathParam("run-id") String runId) throws Exception { - resetLogLevels(request, responder, namespace, appName, appVersion, type, programName, runId); - } - - /** - * Fetches the run record for particular run of a program without version. - * - * @param namespace namespace id - * @param appName application name - * @param type program type - * @param programName program name - * @param runId the run id - * @return run record for the specified program and runRef, null if not found - */ - private RunRecordDetail getRunRecordDetailFromId(String namespace, String appName, - String type, String programName, String runId) - throws BadRequestException, NotFoundException { - ProgramType programType = getProgramType(type); - ProgramReference programRef = new ApplicationReference(namespace, appName).program(programType, - programName); - RunRecordDetail runRecordMeta = store.getRun(programRef, runId); - if (runRecordMeta == null) { - throw new NotFoundException( - String.format("No run record found for program %s and runID: %s", programRef, runId)); - } - return runRecordMeta; - } - /** * 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 @@ -1488,7 +1338,7 @@ private RunRecordDetail getRunRecordDetailFromId(String namespace, String appNam @AuditPolicy(AuditDetail.REQUEST_BODY) public void getStatuses(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List batchPrograms = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); + List batchPrograms = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); List programs = batchPrograms.stream() .map(p -> new ProgramReference(namespaceId, p.getAppId(), p.getProgramType(), p.getProgramId())) @@ -1512,7 +1362,7 @@ public void getStatuses(FullHttpRequest request, HttpResponder responder, null, statuses.get(programId).name())); } } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(result)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(result)); } /** @@ -1548,13 +1398,11 @@ public void getStatuses(FullHttpRequest request, HttpResponder responder, @AuditPolicy({AuditDetail.REQUEST_BODY, AuditDetail.RESPONSE_BODY}) public void stopPrograms(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); + List programs = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); List issuedStops = new ArrayList<>(programs.size()); for (final BatchProgram program : programs) { - ApplicationId applicationId = new ApplicationId(namespaceId, program.getAppId(), - getLatestAppVersion(new NamespaceId(namespaceId), - program.getAppId())); + ApplicationId applicationId = store.getLatestApp(new ApplicationReference(namespaceId, program.getAppId())); ProgramId programId = new ProgramId(applicationId, program.getProgramType(), program.getProgramId()); try { @@ -1572,7 +1420,7 @@ public void stopPrograms(FullHttpRequest request, HttpResponder responder, } } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(issuedStops)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(issuedStops)); } /** @@ -1611,13 +1459,11 @@ public void stopPrograms(FullHttpRequest request, HttpResponder responder, public void startPrograms(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List programs = validateAndGetBatchInput(request, BATCH_STARTS_TYPE); + List programs = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_STARTS_TYPE); List output = new ArrayList<>(programs.size()); for (BatchProgramStart program : programs) { - ApplicationId applicationId = new ApplicationId(namespaceId, program.getAppId(), - getLatestAppVersion(new NamespaceId(namespaceId), - program.getAppId())); + ApplicationId applicationId = store.getLatestApp(new ApplicationReference(namespaceId, program.getAppId())); ProgramId programId = new ProgramId(applicationId, program.getProgramType(), program.getProgramId()); try { @@ -1634,85 +1480,7 @@ public void startPrograms(FullHttpRequest request, HttpResponder responder, new BatchProgramResult(program, HttpResponseStatus.CONFLICT.code(), e.getMessage())); } } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(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. - *

- * Example input: - *


-   * [{"appId": "App1", "programType": "Service", "programId": "Service1", "runnableId": "Runnable1"},
-   *  {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2"}]
-   * 
- *

- * The response will be an array of JsonObjects each of which will contain the three input - * parameters as well as 3 fields: - *

    - *
  • "provisioned" which maps to the number of instances actually provided for the input runnable;
  • - *
  • "requested" which maps to the number of instances the user has requested for the input runnable; and
  • - *
  • "statusCode" which maps to the http status code for the data in that JsonObjects (200, 400, 404).
  • - *
- *

- * 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. - *

- * For example, if there is no Flowlet1 in the above data, then the response could be 200 OK with the following data: - *

- *

-   * [{"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"}]
-   * 
- */ - @POST - @Path("/instances") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void getInstances(FullHttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespaceId) throws IOException, BadRequestException { - - List runnables = validateAndGetBatchInput(request, BATCH_RUNNABLES_TYPE); - - // cache app specs to perform fewer store lookups - Map appSpecs = new HashMap<>(); - - List 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.code(), - String.format("Program type '%s' is not a valid program type to get instances", - runnable.getProgramType().getPrettyName()))); - continue; - } - - ApplicationId appId = new ApplicationId(namespaceId, runnable.getAppId()); - try { - appId = new ApplicationId(namespaceId, runnable.getAppId(), - getLatestAppVersion(new NamespaceId(namespaceId), runnable.getAppId())); - } catch (ApplicationNotFoundException e) { - output.add(new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.code(), - String.format("App: %s not found", appId))); - continue; - } - - // 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); - ProgramId programId = appId.program(runnable.getProgramType(), runnable.getProgramId()); - output.add(getProgramInstances(runnable, spec, programId)); - } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(output)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(output)); } /** @@ -1753,7 +1521,7 @@ public void getInstances(FullHttpRequest request, HttpResponder responder, @Path("/runcount") public void getRunCounts(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); + List programs = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); if (programs.size() > 100) { throw new BadRequestException( String.format("%d programs found in the request, the maximum number " @@ -1784,7 +1552,7 @@ public void getRunCounts(FullHttpRequest request, HttpResponder responder, exception.getMessage(), null)); } } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(counts)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(counts)); } /** @@ -1825,7 +1593,7 @@ public void getRunCounts(FullHttpRequest request, HttpResponder responder, @Path("/runs") public void getLatestRuns(FullHttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { - List programs = validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); + List programs = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE); List programRefs = programs.stream().map( p -> new ProgramReference(namespaceId, p.getAppId(), p.getProgramType(), @@ -1856,7 +1624,7 @@ public void getLatestRuns(FullHttpRequest request, HttpResponder responder, exception.getMessage(), Collections.emptyList())); } } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(response)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(response)); } /** @@ -1869,11 +1637,11 @@ public void getProgramRunCount(HttpRequest request, HttpResponder responder, @PathParam("app-name") String appName, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); ProgramReference programReference = new ProgramReference(namespaceId, appName, programType, programName); long runCount = lifecycleService.getProgramTotalRunCount(programReference); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(runCount)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(runCount)); } /** @@ -1887,7 +1655,7 @@ public void getProgramRunCountVersioned(HttpRequest request, HttpResponder respo @PathParam("app-version") String appVersion, @PathParam("program-type") String type, @PathParam("program-name") String programName) throws Exception { - ProgramType programType = getProgramType(type); + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); long runCount; if (ApplicationId.DEFAULT_VERSION.equals(appVersion)) { ProgramReference programReference = new ProgramReference(namespaceId, appName, programType, @@ -1898,7 +1666,7 @@ public void getProgramRunCountVersioned(HttpRequest request, HttpResponder respo programName); runCount = lifecycleService.getProgramRunCount(programId); } - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(runCount)); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(runCount)); } /* @@ -1913,8 +1681,9 @@ public void getProgramRunCountVersioned(HttpRequest request, HttpResponder respo public void getAllMapReduce(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson( - lifecycleService.list(validateAndGetNamespace(namespaceId), ProgramType.MAPREDUCE))); + ProgramHandlerUtil.toJson( + lifecycleService.list(NamespaceHelper.validateNamespace(namespaceQueryAdmin,namespaceId), + ProgramType.MAPREDUCE))); } /** @@ -1925,8 +1694,9 @@ public void getAllMapReduce(HttpRequest request, HttpResponder responder, public void getAllSpark(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson( - lifecycleService.list(validateAndGetNamespace(namespaceId), ProgramType.SPARK))); + ProgramHandlerUtil.toJson( + lifecycleService.list(NamespaceHelper.validateNamespace(namespaceQueryAdmin,namespaceId), + ProgramType.SPARK))); } /** @@ -1937,8 +1707,9 @@ public void getAllSpark(HttpRequest request, HttpResponder responder, public void getAllWorkflows(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson( - lifecycleService.list(validateAndGetNamespace(namespaceId), ProgramType.WORKFLOW))); + ProgramHandlerUtil.toJson( + lifecycleService.list(NamespaceHelper.validateNamespace(namespaceQueryAdmin,namespaceId), + ProgramType.WORKFLOW))); } /** @@ -1949,8 +1720,9 @@ public void getAllWorkflows(HttpRequest request, HttpResponder responder, public void getAllServices(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson( - lifecycleService.list(validateAndGetNamespace(namespaceId), ProgramType.SERVICE))); + ProgramHandlerUtil.toJson( + lifecycleService.list(NamespaceHelper.validateNamespace(namespaceQueryAdmin,namespaceId), + ProgramType.SERVICE))); } @GET @@ -1958,107 +1730,9 @@ public void getAllServices(HttpRequest request, HttpResponder responder, public void getAllWorkers(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) throws Exception { responder.sendJson(HttpResponseStatus.OK, - GSON.toJson( - lifecycleService.list(validateAndGetNamespace(namespaceId), ProgramType.WORKER))); - } - - /** - * 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) throws Exception { - try { - ProgramId programId = validateAndGetNamespace(namespaceId) - .app(appId, getLatestAppVersion(new NamespaceId(namespaceId), appId)) - .worker(workerId); - lifecycleService.ensureProgramExists(programId); - int count = store.getWorkerInstances(programId); - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(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") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void setWorkerInstances(FullHttpRequest 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(new ApplicationId(namespaceId, appId, - getLatestAppVersion(new NamespaceId(namespaceId), appId)) - .program(ProgramType.WORKER, workerId), instances); - 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) - throws BadRequestException, ApplicationNotFoundException { - ProgramType type = getProgramType(programCategory); - ProgramId program = new ApplicationId(namespaceId, appId, - getLatestAppVersion(new NamespaceId(namespaceId), appId)) - .program(type, programId); - getLiveInfo(responder, program, runtimeService); - } - - - private void getLiveInfo(HttpResponder responder, ProgramId programId, - ProgramRuntimeService runtimeService) { - try { - responder.sendJson(HttpResponseStatus.OK, GSON.toJson(runtimeService.getLiveInfo(programId))); - } 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) throws Exception { - try { - ProgramId programId = validateAndGetNamespace(namespaceId) - .app(appId, getLatestAppVersion(new NamespaceId(namespaceId), appId)) - .service(serviceId); - lifecycleService.ensureProgramExists(programId); - int instances = store.getServiceInstances(programId); - responder.sendJson(HttpResponseStatus.OK, - GSON.toJson(new ServiceInstances(instances, getInstanceCount(programId, serviceId)))); - } catch (SecurityException e) { - responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); - } + ProgramHandlerUtil.toJson( + lifecycleService.list(NamespaceHelper.validateNamespace(namespaceQueryAdmin,namespaceId), + ProgramType.WORKER))); } /** @@ -2091,7 +1765,7 @@ public void getServiceAvailabilityVersioned(HttpRequest request, HttpResponder r @PathParam("service-type") String serviceType, @PathParam("program-name") String programName) throws Exception { // Currently, we only support services and sparks as the service-type - ProgramType programType = getProgramType(serviceType); + ProgramType programType = ProgramType.valueOfCategoryName(serviceType, BadRequestException::new); if (!ServiceDiscoverable.getUserServiceTypes().contains(programType)) { throw new BadRequestException( "Only service or spark is support for service availability check"); @@ -2128,230 +1802,14 @@ public void getServiceAvailabilityVersioned(HttpRequest request, HttpResponder r responder.sendString(HttpResponseStatus.OK, "Service is available to accept requests."); } - /** - * Set instances of a service. - */ - @PUT - @Path("/apps/{app-id}/services/{service-id}/instances") - @AuditPolicy(AuditDetail.REQUEST_BODY) - public void setServiceInstances(FullHttpRequest request, HttpResponder responder, - @PathParam("namespace-id") String namespaceId, - @PathParam("app-id") String appId, - @PathParam("service-id") String serviceId) throws Exception { - try { - ProgramId programId = new ApplicationId(namespaceId, appId, - getLatestAppVersion(new NamespaceId(namespaceId), appId)) - .program(ProgramType.SERVICE, serviceId); - Store.ensureProgramExists(programId, store.getApplication(programId.getParent())); - - 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; - } - } - - /** - * 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, - ProgramId programId) { - int requested; - String programName = programId.getProgram(); - ProgramType programType = programId.getType(); - if (programType == ProgramType.WORKER) { - if (!spec.getWorkers().containsKey(programName)) { - return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.code(), - "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.code(), - "Service: " + programName + " not found"); - } - requested = spec.getServices().get(programName).getInstances(); - - } else { - return new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.code(), - "Instances not supported for program type + " + programType); - } - int provisioned = getInstanceCount(programId, programName); - // use the pretty name of program types to be consistent - return new BatchRunnableInstances(runnable, HttpResponseStatus.OK.code(), provisioned, - requested); - } - - /** - * 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); - 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); - } - - // Not running on YARN default 1 - return 1; - } - - private int getRequestedServiceInstances(ProgramId serviceId) { - // Not running on YARN, get it from store - return store.getServiceInstances(serviceId); - } - private boolean isDebugAllowed(ProgramType programType) { return EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programType); } - private boolean canHaveInstances(ProgramType programType) { - return EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programType); - } - - private List validateAndGetBatchInput(FullHttpRequest request, - Type type) - throws BadRequestException, IOException { - - List programs; - try (Reader reader = new InputStreamReader(new ByteBufInputStream(request.content()), - StandardCharsets.UTF_8)) { - try { - programs = DECODE_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; - } - - private void updateLogLevels(FullHttpRequest request, HttpResponder responder, String namespace, - String appName, - String appVersion, String type, String programName, - String runId) throws Exception { - ProgramType programType = getProgramType(type); - try { - // we are decoding the body to Map instead of Map here since Gson will - // serialize invalid enum values to null, which is allowed for log level, instead of throw an Exception. - lifecycleService.updateProgramLogLevels( - new ApplicationId(namespace, appName, appVersion).program(programType, programName), - transformLogLevelsMap(decodeArguments(request)), runId); - responder.sendStatus(HttpResponseStatus.OK); - } catch (JsonSyntaxException e) { - throw new BadRequestException("Invalid JSON in body"); - } catch (IllegalArgumentException e) { - throw new BadRequestException(e.getMessage()); - } catch (SecurityException e) { - throw new UnauthorizedException("Unauthorized to update the log levels"); - } - } - - private void resetLogLevels(FullHttpRequest request, HttpResponder responder, String namespace, - String appName, - String appVersion, String type, String programName, - String runId) throws Exception { - ProgramType programType = getProgramType(type); - try { - Set loggerNames = parseBody(request, SET_STRING_TYPE); - lifecycleService.resetProgramLogLevels( - new ApplicationId(namespace, appName, appVersion).program(programType, programName), - loggerNames == null ? Collections.emptySet() : loggerNames, runId); - responder.sendStatus(HttpResponseStatus.OK); - } catch (JsonSyntaxException e) { - throw new BadRequestException("Invalid JSON in body"); - } catch (SecurityException e) { - throw new UnauthorizedException("Unauthorized to reset the log levels"); - } - } - - private NamespaceId validateAndGetNamespace(String namespace) throws NamespaceNotFoundException { - NamespaceId namespaceId = new NamespaceId(namespace); - try { - namespaceQueryAdmin.get(namespaceId); - } catch (NamespaceNotFoundException e) { - throw e; - } catch (Exception e) { - // This can only happen when NamespaceAdmin uses HTTP to interact with namespaces. - // Within AppFabric, NamespaceAdmin is bound to DefaultNamespaceAdmin which directly interacts with MDS. - // Hence, this should never happen. - throw Throwables.propagate(e); - } - return namespaceId; - } - - /** - * Parses the give program type into {@link ProgramType} object. - * - * @param programType the program type to parse. - * @throws BadRequestException if the given program type is not a valid {@link ProgramType}. - */ - private ProgramType getProgramType(String programType) throws BadRequestException { - try { - return ProgramType.valueOfCategoryName(programType); - } catch (Exception e) { - throw new BadRequestException(String.format("Invalid program type '%s'", programType), e); - } - } - /** * Used to filter out RunRecords initiated by a tethered instance */ private boolean isTetheredRunRecord(RunRecord runRecord) { return runRecord.getPeerName() != null; } - - /** - * @param namespaceId namespace Id - * @param appId app Id - * @return latest app version - */ - private String getLatestAppVersion(NamespaceId namespaceId, String appId) - throws ApplicationNotFoundException { - ApplicationMeta latestApplicationMeta = store.getLatest(namespaceId.appReference(appId)); - if (latestApplicationMeta == null) { - throw new ApplicationNotFoundException( - new ApplicationReference(namespaceId.getNamespace(), appId)); - } - return latestApplicationMeta.getSpec().getAppVersion(); - } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramRuntimeHttpHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramRuntimeHttpHandler.java new file mode 100644 index 000000000000..a6cc26a7d0ce --- /dev/null +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/ProgramRuntimeHttpHandler.java @@ -0,0 +1,566 @@ +/* + * Copyright © 2025 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 io.cdap.cdap.gateway.handlers; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.cdap.cdap.api.app.ApplicationSpecification; +import io.cdap.cdap.app.runtime.ProgramRuntimeService; +import io.cdap.cdap.app.store.Store; +import io.cdap.cdap.common.ApplicationNotFoundException; +import io.cdap.cdap.common.BadRequestException; +import io.cdap.cdap.common.NotFoundException; +import io.cdap.cdap.common.conf.Constants; +import io.cdap.cdap.common.namespace.NamespaceQueryAdmin; +import io.cdap.cdap.common.security.AuditDetail; +import io.cdap.cdap.common.security.AuditPolicy; +import io.cdap.cdap.gateway.handlers.util.AbstractAppFabricHttpHandler; +import io.cdap.cdap.gateway.handlers.util.NamespaceHelper; +import io.cdap.cdap.gateway.handlers.util.ProgramHandlerUtil; +import io.cdap.cdap.internal.app.services.ProgramLifecycleService; +import io.cdap.cdap.internal.app.store.RunRecordDetail; +import io.cdap.cdap.proto.BatchRunnable; +import io.cdap.cdap.proto.BatchRunnableInstances; +import io.cdap.cdap.proto.Containers; +import io.cdap.cdap.proto.Instances; +import io.cdap.cdap.proto.NotRunningProgramLiveInfo; +import io.cdap.cdap.proto.ProgramLiveInfo; +import io.cdap.cdap.proto.ProgramType; +import io.cdap.cdap.proto.ServiceInstances; +import io.cdap.cdap.proto.id.ApplicationId; +import io.cdap.cdap.proto.id.ApplicationReference; +import io.cdap.cdap.proto.id.ProgramId; +import io.cdap.cdap.proto.id.ProgramReference; +import io.cdap.cdap.proto.security.StandardPermission; +import io.cdap.cdap.security.spi.authentication.AuthenticationContext; +import io.cdap.cdap.security.spi.authorization.AccessEnforcer; +import io.cdap.cdap.security.spi.authorization.UnauthorizedException; +import io.cdap.http.HttpResponder; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +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; + +/** + * {@link io.cdap.http.HttpHandler} to manage runtime of Programs for v3 REST APIs + * + * Only supported program types for this handler are {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. + */ +@Singleton +@Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}") +public class ProgramRuntimeHttpHandler extends AbstractAppFabricHttpHandler { + + private final ProgramLifecycleService lifecycleService; + private final ProgramRuntimeService runtimeService; + private final Store store; + private final NamespaceQueryAdmin namespaceQueryAdmin; + private final AccessEnforcer accessEnforcer; + private final AuthenticationContext authenticationContext; + + @Inject + public ProgramRuntimeHttpHandler(ProgramLifecycleService lifecycleService, Store store, + ProgramRuntimeService runtimeService, NamespaceQueryAdmin namespaceQueryAdmin, AccessEnforcer accessEnforcer, + AuthenticationContext authenticationContext) { + this.lifecycleService = lifecycleService; + this.runtimeService = runtimeService; + this.store = store; + this.namespaceQueryAdmin = namespaceQueryAdmin; + this.accessEnforcer = accessEnforcer; + this.authenticationContext = authenticationContext; + } + + private static final Type BATCH_RUNNABLES_TYPE = new TypeToken>() { + }.getType(); + + /** + * 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. + *

+ * Example input: + *


+   * [{"appId": "App1", "programType": "Service", "programId": "Service1", "runnableId": "Runnable1"},
+   *  {"appId": "App1", "programType": "Mapreduce", "programId": "Mapreduce2"}]
+   * 
+ *

+ * The response will be an array of JsonObjects each of which will contain the three input + * parameters as well as 3 fields: + *

    + *
  • "provisioned" which maps to the number of instances actually provided for the input runnable;
  • + *
  • "requested" which maps to the number of instances the user has requested for the input runnable; and
  • + *
  • "statusCode" which maps to the http status code for the data in that JsonObjects (200, 400, 404).
  • + *
+ *

+ * 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. + *

+ * For example, if there is no Flowlet1 in the above data, then the response could be 200 OK with the following data: + *

+ *

+   * [{"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"}]
+   * 
+ */ + @POST + @Path("/instances") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void getInstances(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespaceId) throws IOException, BadRequestException { + + List runnables = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_RUNNABLES_TYPE); + + // cache app specs to perform fewer store lookups + Map appSpecs = new HashMap<>(); + + List output = new ArrayList<>(runnables.size()); + for (BatchRunnable runnable : runnables) { + // cant get instances for things that are not services, or workers + if (!canHaveInstances(runnable.getProgramType())) { + output.add( + new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.code(), + String.format("Program type '%s' is not a valid program type to get instances", + runnable.getProgramType().getPrettyName()))); + continue; + } + + ApplicationId appId = new ApplicationId(namespaceId, runnable.getAppId()); + try { + appId = store.getLatestApp(new ApplicationReference(namespaceId, runnable.getAppId())); + } catch (ApplicationNotFoundException e) { + output.add(new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.code(), + String.format("App: %s not found", appId))); + continue; + } + + // 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); + ProgramId programId = appId.program(runnable.getProgramType(), runnable.getProgramId()); + output.add(getProgramInstances(runnable, spec, programId)); + } + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(output)); + } + + /** + * 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) throws Exception { + try { + NamespaceHelper.validateNamespace(namespaceQueryAdmin, namespaceId); + ProgramId programId = store.getLatestApp(new ApplicationReference(namespaceId, appId)).service(serviceId); + lifecycleService.ensureProgramExists(programId); + int instances = store.getServiceInstances(programId); + responder.sendJson(HttpResponseStatus.OK, + ProgramHandlerUtil.toJson(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") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void setServiceInstances(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespaceId, + @PathParam("app-id") String appId, + @PathParam("service-id") String serviceId) throws Exception { + try { + ProgramId programId = store.getLatestApp(new ApplicationReference(namespaceId, appId)) + .program(ProgramType.SERVICE, serviceId); + Store.ensureProgramExists(programId, store.getApplication(programId.getParent())); + int instances = getInstances(request); + accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE); + setInstances(programId, instances); + responder.sendStatus(HttpResponseStatus.OK); + } catch (SecurityException e) { + responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); + } catch (Throwable throwable) { + if (respondIfElementNotFound(throwable, responder)) { + return; + } + throw throwable; + } + } + + /** + * 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) throws Exception { + try { + NamespaceHelper.validateNamespace(namespaceQueryAdmin, namespaceId); + ProgramId programId = store.getLatestApp(new ApplicationReference(namespaceId, appId)).worker(workerId); + lifecycleService.ensureProgramExists(programId); + int count = store.getWorkerInstances(programId); + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(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") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void setWorkerInstances(FullHttpRequest 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 { + ProgramId programId = store.getLatestApp(new ApplicationReference(namespaceId, appId)) + .program(ProgramType.WORKER, workerId); + Store.ensureProgramExists(programId, store.getApplication(programId.getParent())); + accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE); + setInstances(programId, instances); + responder.sendStatus(HttpResponseStatus.OK); + } catch (SecurityException e) { + responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); + } catch (Throwable e) { + if (respondIfElementNotFound(e, responder)) { + return; + } + throw e; + } + } + + /** + * Gets runtime information about a running program. + * + * @param request the HTTP request + * @param responder the HTTP responder + * @param namespaceId namespaceId for the program + * @param appId appId for the program + * @param programCategory program type + * @param programId the program Id + * + * @throws BadRequestException + * @throws ApplicationNotFoundException + */ + @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) + throws BadRequestException, ApplicationNotFoundException { + ProgramType type = ProgramType.valueOfCategoryName(programCategory, BadRequestException::new); + ProgramId program = store.getLatestApp(new ApplicationReference(namespaceId, appId)) + .program(type, programId); + getLiveInfo(responder, program, runtimeService); + } + + /** + * Update the log level for a running program according to the request body. Currently supported + * program types are {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. The request body + * is expected to contain a map of log levels, where key is loggername, value is one of the valid + * {@link org.apache.twill.api.logging.LogEntry.Level} or null. + * + */ + @PUT + @Path("/apps/{app-name}/{program-type}/{program-name}/runs/{run-id}/loglevels") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void updateProgramLogLevels(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespace, + @PathParam("app-name") String appName, + @PathParam("program-type") String type, + @PathParam("program-name") String programName, + @PathParam("run-id") String runId) throws Exception { + RunRecordDetail run = getRunRecordDetailFromId(namespace, appName, type, programName, runId); + updateLogLevels(request, responder, namespace, appName, run.getVersion(), type, programName, + runId); + } + + /** + * Update the log level for a running program according to the request body. + * Deprecated : Run-id is sufficient to identify a program run. + */ + @Deprecated + @PUT + @Path("/apps/{app-name}/versions/{app-version}/{program-type}/{program-name}/runs/{run-id}/loglevels") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void updateProgramLogLevelsVersioned(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespace, + @PathParam("app-name") String appName, + @PathParam("app-version") String appVersion, + @PathParam("program-type") String type, + @PathParam("program-name") String programName, + @PathParam("run-id") String runId) throws Exception { + updateLogLevels(request, responder, namespace, appName, appVersion, type, programName, runId); + } + + /** + * Reset the log level for a running program back to where it starts. Currently supported program + * types are {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. The request body can + * either be empty, which will reset all loggers for the program, or contain a list of logger + * names, which will reset for these particular logger names for the program. + */ + @POST + @Path("/apps/{app-name}/{program-type}/{program-name}/runs/{run-id}/resetloglevels") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void resetProgramLogLevels(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespace, + @PathParam("app-name") String appName, + @PathParam("program-type") String type, + @PathParam("program-name") String programName, + @PathParam("run-id") String runId) throws Exception { + RunRecordDetail run = getRunRecordDetailFromId(namespace, appName, type, programName, runId); + resetLogLevels(request, responder, namespace, appName, run.getVersion(), type, programName, + runId); + } + + /** + * Reset the log level for a running program back to where it starts. + * + * Deprecated : Run-id is sufficient to identify a program run. + */ + @POST + @Path("/apps/{app-name}/versions/{app-version}/{program-type}/{program-name}/runs/{run-id}/resetloglevels") + @AuditPolicy(AuditDetail.REQUEST_BODY) + public void resetProgramLogLevelsVersioned(FullHttpRequest request, HttpResponder responder, + @PathParam("namespace-id") String namespace, + @PathParam("app-name") String appName, + @PathParam("app-version") String appVersion, + @PathParam("program-type") String type, + @PathParam("program-name") String programName, + @PathParam("run-id") String runId) throws Exception { + resetLogLevels(request, responder, namespace, appName, appVersion, type, programName, runId); + } + + private void getLiveInfo(HttpResponder responder, ProgramId programId, + ProgramRuntimeService runtimeService) { + try { + responder.sendJson(HttpResponseStatus.OK, ProgramHandlerUtil.toJson(runtimeService.getLiveInfo(programId))); + } catch (SecurityException e) { + responder.sendStatus(HttpResponseStatus.UNAUTHORIZED); + } + } + + /** + * 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); + 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); + } + + // Not running on YARN default 1 + return 1; + } + + /** + * 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, + ProgramId programId) { + int requested; + String programName = programId.getProgram(); + ProgramType programType = programId.getType(); + if (programType == ProgramType.WORKER) { + if (!spec.getWorkers().containsKey(programName)) { + return new BatchRunnableInstances(runnable, HttpResponseStatus.NOT_FOUND.code(), + "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.code(), + "Service: " + programName + " not found"); + } + requested = spec.getServices().get(programName).getInstances(); + + } else { + return new BatchRunnableInstances(runnable, HttpResponseStatus.BAD_REQUEST.code(), + "Instances not supported for program type + " + programType); + } + int provisioned = getInstanceCount(programId, programName); + // use the pretty name of program types to be consistent + return new BatchRunnableInstances(runnable, HttpResponseStatus.OK.code(), provisioned, + requested); + } + + private int getRequestedServiceInstances(ProgramId serviceId) { + // Not running on YARN, get it from store + return store.getServiceInstances(serviceId); + } + + private boolean canHaveInstances(ProgramType programType) { + return EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programType); + } + + private void resetLogLevels(FullHttpRequest request, HttpResponder responder, String namespace, + String appName, + String appVersion, String type, String programName, + String runId) throws Exception { + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + try { + Set loggerNames = parseBody(request, SET_STRING_TYPE); + ProgramId programId = new ApplicationId(namespace, appName, appVersion).program(programType, programName); + accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE); + runtimeService.resetProgramLogLevels( + new ApplicationId(namespace, appName, appVersion).program(programType, programName), + loggerNames == null ? Collections.emptySet() : loggerNames, runId); + responder.sendStatus(HttpResponseStatus.OK); + } catch (JsonSyntaxException e) { + throw new BadRequestException("Invalid JSON in body"); + } catch (SecurityException e) { + throw new UnauthorizedException("Unauthorized to reset the log levels"); + } + } + + private void updateLogLevels(FullHttpRequest request, HttpResponder responder, String namespace, + String appName, + String appVersion, String type, String programName, + String runId) throws Exception { + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + try { + // we are decoding the body to Map instead of Map here since Gson will + // serialize invalid enum values to null, which is allowed for log level, instead of throw an Exception. + ProgramId programId = new ApplicationId(namespace, appName, appVersion).program(programType, programName); + accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.UPDATE); + runtimeService.updateProgramLogLevels( + new ApplicationId(namespace, appName, appVersion).program(programType, programName), + transformLogLevelsMap(decodeArguments(request)), runId); + responder.sendStatus(HttpResponseStatus.OK); + } catch (JsonSyntaxException e) { + throw new BadRequestException("Invalid JSON in body"); + } catch (IllegalArgumentException e) { + throw new BadRequestException(e.getMessage()); + } catch (SecurityException e) { + throw new UnauthorizedException("Unauthorized to update the log levels"); + } + } + + /** + * Set instances for the given program. Only supported program types for this action are {@link + * ProgramType#SERVICE} and {@link ProgramType#WORKER}. + * + * @param programId the {@link ProgramId} of the program for which instances are to be + * updated + * @param instances the number of instances to be updated. + * @throws InterruptedException if there is an error while asynchronously updating instances + * @throws ExecutionException if there is an error while asynchronously updating instances + * @throws BadRequestException if the number of instances specified is less than 0 + * @throws UnauthorizedException if the user does not have privileges to set instances for the + * specified program. To set instances for a program, a user needs {@link + * StandardPermission#UPDATE} on the program. + */ + private void setInstances(ProgramId programId, int instances) throws Exception { + if (instances < 1) { + throw new BadRequestException( + String.format("Instance count should be greater than 0. Got %s.", instances)); + } + switch (programId.getType()) { + case SERVICE: + int oldInstances = store.getServiceInstances(programId); + if (oldInstances != instances) { + store.setServiceInstances(programId, instances); + runtimeService.setInstances(programId, instances, instances); + } + break; + case WORKER: + oldInstances = store.getWorkerInstances(programId); + if (oldInstances != instances) { + store.setWorkerInstances(programId, instances); + runtimeService.setInstances(programId, instances, instances); + } + break; + default: + throw new BadRequestException( + String.format("Setting instances for program type %s is not supported", + programId.getType().getPrettyName())); + } + } + + private RunRecordDetail getRunRecordDetailFromId(String namespaceId, String appName, String type, + String programName, String runId) throws NotFoundException, BadRequestException { + ProgramType programType = ProgramType.valueOfCategoryName(type, BadRequestException::new); + ProgramReference programRef = new ApplicationReference(namespaceId, appName).program(programType, + programName); + RunRecordDetail runRecordMeta = store.getRun(programRef, runId); + if (runRecordMeta == null) { + throw new NotFoundException( + String.format("No run record found for program %s and runID: %s", programRef, runId)); + } + return runRecordMeta; + } +} diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/NamespaceHelper.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/NamespaceHelper.java new file mode 100644 index 000000000000..63224673bef9 --- /dev/null +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/NamespaceHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2025 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 io.cdap.cdap.gateway.handlers.util; + +import com.google.common.base.Throwables; +import io.cdap.cdap.common.NamespaceNotFoundException; +import io.cdap.cdap.common.namespace.NamespaceQueryAdmin; +import io.cdap.cdap.proto.id.NamespaceId; + +/** + * Helper class for Namespace operations. + */ +public class NamespaceHelper { + + private NamespaceHelper() { + } + + /** + * Validates that the namespace exists and gets the NamespaceId + * + * @param namespaceQueryAdmin query admin for namespace operations + * @param namespace the namespace to validate + * @return NamespaceId + * + * @throws NamespaceNotFoundException if namespace is not found + */ + public static NamespaceId validateNamespace(NamespaceQueryAdmin namespaceQueryAdmin, String namespace) + throws NamespaceNotFoundException { + NamespaceId namespaceId = new NamespaceId(namespace); + try { + namespaceQueryAdmin.get(namespaceId); + } catch (NamespaceNotFoundException e) { + throw e; + } catch (Exception e) { + // This can only happen when NamespaceAdmin uses HTTP to interact with namespaces. + // Within AppFabric, NamespaceAdmin is bound to DefaultNamespaceAdmin which directly interacts with MDS. + // Hence, this should never happen. + throw Throwables.propagate(e); + } + return namespaceId; + } +} diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/ProgramHandlerUtil.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/ProgramHandlerUtil.java new file mode 100644 index 000000000000..24055e70eeea --- /dev/null +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/util/ProgramHandlerUtil.java @@ -0,0 +1,119 @@ +/* + * Copyright © 2025 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 io.cdap.cdap.gateway.handlers.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; +import com.google.inject.Inject; +import io.cdap.cdap.api.schedule.Trigger; +import io.cdap.cdap.app.store.Store; +import io.cdap.cdap.common.BadRequestException; +import io.cdap.cdap.common.io.CaseInsensitiveEnumTypeAdapterFactory; +import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter; +import io.cdap.cdap.internal.app.runtime.schedule.constraint.ConstraintCodec; +import io.cdap.cdap.internal.app.runtime.schedule.trigger.SatisfiableTrigger; +import io.cdap.cdap.internal.app.runtime.schedule.trigger.TriggerCodec; +import io.cdap.cdap.internal.schedule.constraint.Constraint; +import io.cdap.cdap.proto.BatchProgram; +import io.cdap.cdap.security.spi.authentication.AuthenticationContext; +import io.cdap.cdap.security.spi.authorization.AccessEnforcer; +import io.netty.buffer.ByteBufInputStream; +import io.netty.handler.codec.http.FullHttpRequest; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; + +public class ProgramHandlerUtil { + + private ProgramHandlerUtil() { + } + + /** + * Json serializer/deserializer. + */ + private static final Gson GSON = ApplicationSpecificationAdapter + .addTypeAdapters(new GsonBuilder()) + .registerTypeAdapter(Trigger.class, new TriggerCodec()) + .registerTypeAdapter(SatisfiableTrigger.class, new TriggerCodec()) + .registerTypeAdapter(Constraint.class, new ConstraintCodec()) + .create(); + + /** + * Json serde for decoding request. It uses a case insensitive enum adapter. + */ + private static final Gson DECODE_GSON = ApplicationSpecificationAdapter + .addTypeAdapters(new GsonBuilder()) + .registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()) + .registerTypeAdapter(Trigger.class, new TriggerCodec()) + .registerTypeAdapter(SatisfiableTrigger.class, new TriggerCodec()) + .registerTypeAdapter(Constraint.class, new ConstraintCodec()) + .create(); + + public static String toJson(Object object) { + return GSON.toJson(object); + } + + public static String toJson(Object object, @NotNull Type type) { + return GSON.toJson(object, type); + } + + public static T fromJson(@NotNull Reader reader, Class type) { + return DECODE_GSON.fromJson(reader, type); + } + + public static T fromJson(@Nullable JsonElement json, Class type) { + return DECODE_GSON.fromJson(json, type); + } + + public static List validateAndGetBatchInput(FullHttpRequest request, + Type type) + throws BadRequestException, IOException { + + List programs; + try (Reader reader = new InputStreamReader(new ByteBufInputStream(request.content()), + StandardCharsets.UTF_8)) { + try { + programs = DECODE_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; + } +} diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRunner.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRunner.java index 3d650c7acfc8..85549cb62c1e 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRunner.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRunner.java @@ -46,6 +46,7 @@ import io.cdap.cdap.data2.dataset2.lib.table.leveldb.LevelDBTableService; import io.cdap.cdap.internal.app.deploy.ProgramTerminator; import io.cdap.cdap.internal.app.runtime.AbstractListener; +import io.cdap.cdap.internal.app.runtime.ProgramStartRequest; import io.cdap.cdap.internal.app.services.ApplicationLifecycleService; import io.cdap.cdap.internal.app.services.ProgramLifecycleService; import io.cdap.cdap.internal.app.services.ProgramNotificationSubscriberService; @@ -208,7 +209,10 @@ public Future startPreview(PreviewRequest previewRequest) throws } LOG.debug("Starting preview for {}", programId); - ProgramController controller = programLifecycleService.start(programId, userProps, false, true); + ProgramStartRequest startRequest = programLifecycleService.prepareStart(programId, userProps, false, true); + ProgramController controller = programRuntimeService.run( + startRequest.getProgramDescriptor(), startRequest.getProgramOptions(), startRequest.getRunId()) + .getController(); long startTimeMillis = System.currentTimeMillis(); AtomicBoolean timeout = new AtomicBoolean(); diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramStartRequest.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramStartRequest.java new file mode 100644 index 000000000000..84814d5a8f10 --- /dev/null +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramStartRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2025 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 io.cdap.cdap.internal.app.runtime; + +import io.cdap.cdap.app.program.ProgramDescriptor; +import io.cdap.cdap.app.runtime.ProgramOptions; +import io.cdap.cdap.common.app.RunIds; +import io.cdap.cdap.proto.id.ProgramRunId; +import org.apache.twill.api.RunId; + +/** + * Request object for starting a new Program run. + */ +public class ProgramStartRequest { + + private final ProgramOptions programOptions; + private final ProgramDescriptor programDescriptor; + private final RunId runId; + + public ProgramStartRequest(ProgramOptions programOptions, + ProgramDescriptor programDescriptor, + ProgramRunId programRunId) { + this.programOptions = programOptions; + this.programDescriptor = programDescriptor; + this.runId = RunIds.fromString(programRunId.getRun()); + } + + public ProgramOptions getProgramOptions() { + return programOptions; + } + + public ProgramDescriptor getProgramDescriptor() { + return programDescriptor; + } + + public RunId getRunId() { + return runId; + } +} diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/service/InMemoryProgramRuntimeService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/service/InMemoryProgramRuntimeService.java index 38a1019ce455..eef7abc6eed7 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/service/InMemoryProgramRuntimeService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/service/InMemoryProgramRuntimeService.java @@ -47,9 +47,8 @@ public final class InMemoryProgramRuntimeService extends AbstractProgramRuntimeS @Inject InMemoryProgramRuntimeService(CConfiguration cConf, ProgramRunnerFactory programRunnerFactory, - ProgramStateWriter programStateWriter, - ProgramRunDispatcherFactory programRunDispatcherFactory) { - super(cConf, programRunnerFactory, programStateWriter, programRunDispatcherFactory); + ProgramStateWriter programStateWriter, ProgramRunDispatcherFactory programRunDispatcherFactory) { + super(cConf, programRunnerFactory, programStateWriter, programRunDispatcherFactory ); } @Override diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramLifecycleService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramLifecycleService.java index c8bdd8b0c19f..8ee093574b07 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramLifecycleService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramLifecycleService.java @@ -17,7 +17,6 @@ package io.cdap.cdap.internal.app.services; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -29,11 +28,8 @@ import io.cdap.cdap.api.plugin.Plugin; import io.cdap.cdap.app.guice.ClusterMode; import io.cdap.cdap.app.program.ProgramDescriptor; -import io.cdap.cdap.app.runtime.LogLevelUpdater; -import io.cdap.cdap.app.runtime.ProgramController; import io.cdap.cdap.app.runtime.ProgramOptions; import io.cdap.cdap.app.runtime.ProgramRuntimeService; -import io.cdap.cdap.app.runtime.ProgramRuntimeService.RuntimeInfo; import io.cdap.cdap.app.runtime.ProgramStateWriter; import io.cdap.cdap.app.store.ScanApplicationsRequest; import io.cdap.cdap.app.store.Store; @@ -54,6 +50,7 @@ import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter; import io.cdap.cdap.internal.app.runtime.BasicArguments; import io.cdap.cdap.internal.app.runtime.ProgramOptionConstants; +import io.cdap.cdap.internal.app.runtime.ProgramStartRequest; import io.cdap.cdap.internal.app.runtime.SimpleProgramOptions; import io.cdap.cdap.internal.app.runtime.SystemArguments; import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository; @@ -67,7 +64,6 @@ import io.cdap.cdap.internal.provision.ProvisioningService; import io.cdap.cdap.proto.ProgramHistory; import io.cdap.cdap.proto.ProgramRecord; -import io.cdap.cdap.proto.ProgramRunClusterStatus; import io.cdap.cdap.proto.ProgramRunStatus; import io.cdap.cdap.proto.ProgramStatus; import io.cdap.cdap.proto.ProgramType; @@ -112,7 +108,6 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.twill.api.RunId; -import org.apache.twill.api.logging.LogEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -134,27 +129,27 @@ public class ProgramLifecycleService { ProgramRunStatus.SUSPENDED); private final Store store; - private final ProfileService profileService; - private final ProgramRuntimeService runtimeService; - private final PropertiesResolver propertiesResolver; - private final PreferencesService preferencesService; + private final ProgramStateWriter programStateWriter; private final AccessEnforcer accessEnforcer; private final AuthenticationContext authenticationContext; + private final PropertiesResolver propertiesResolver; + private final CapabilityReader capabilityReader; + private final ArtifactRepository artifactRepository; + private final ProfileService profileService; + private final PreferencesService preferencesService; private final ProvisionerNotifier provisionerNotifier; private final ProvisioningService provisioningService; - private final ProgramStateWriter programStateWriter; - private final CapabilityReader capabilityReader; + private final FlowControlService flowControlService; private final int maxConcurrentRuns; private final int maxConcurrentLaunching; private final int defaultStopTimeoutSecs; private final int batchSize; - private final ArtifactRepository artifactRepository; - private final FlowControlService flowControlService; + private final boolean userProgramLaunchDisabled; @Inject ProgramLifecycleService(CConfiguration cConf, - Store store, ProfileService profileService, ProgramRuntimeService runtimeService, + Store store, ProfileService profileService, PropertiesResolver propertiesResolver, PreferencesService preferencesService, AccessEnforcer accessEnforcer, AuthenticationContext authenticationContext, @@ -162,45 +157,26 @@ public class ProgramLifecycleService { ProgramStateWriter programStateWriter, CapabilityReader capabilityReader, ArtifactRepository artifactRepository, FlowControlService flowControlService) { + this.store = store; + this.programStateWriter = programStateWriter; + this.accessEnforcer = accessEnforcer; + this.authenticationContext = authenticationContext; + this.propertiesResolver = propertiesResolver; + this.capabilityReader = capabilityReader; + this.artifactRepository = artifactRepository; this.maxConcurrentRuns = cConf.getInt(Constants.AppFabric.MAX_CONCURRENT_RUNS); this.maxConcurrentLaunching = cConf.getInt(Constants.AppFabric.MAX_CONCURRENT_LAUNCHING); this.defaultStopTimeoutSecs = cConf.getInt(Constants.AppFabric.PROGRAM_MAX_STOP_SECONDS); this.userProgramLaunchDisabled = cConf.getBoolean( Constants.AppFabric.USER_PROGRAM_LAUNCH_DISABLED, false); this.batchSize = cConf.getInt(Constants.AppFabric.STREAMING_BATCH_SIZE); - this.store = store; this.profileService = profileService; - this.runtimeService = runtimeService; - this.propertiesResolver = propertiesResolver; this.preferencesService = preferencesService; - this.accessEnforcer = accessEnforcer; - this.authenticationContext = authenticationContext; this.provisionerNotifier = provisionerNotifier; this.provisioningService = provisioningService; - this.programStateWriter = programStateWriter; - this.capabilityReader = capabilityReader; - this.artifactRepository = artifactRepository; this.flowControlService = flowControlService; } - /** - * Returns the program status. - * - * @param programId the id of the program for which the status call is made - * @return the status of the program - * @throws NotFoundException if the application to which this program belongs was not found - */ - public ProgramStatus getProgramStatus(ProgramId programId) throws Exception { - // check that app exists - ApplicationId appId = programId.getParent(); - ApplicationSpecification appSpec = store.getApplication(appId); - if (appSpec == null) { - throw new NotFoundException(appId); - } - - return getExistingAppProgramStatus(appSpec, programId); - } - /** * Returns the status of the latest version program. * @@ -214,26 +190,21 @@ public ProgramStatus getProgramStatus(ProgramReference programReference) throws } /** - * Returns the program status based on the active run records of a program. A program is RUNNING - * if there are any RUNNING, STOPPING, or SUSPENDED run records. A program is starting if there - * are any PENDING or STARTING run records and no RUNNING run records. Otherwise, it is STOPPED. + * Returns the program status. * - * @param runRecords run records for the program - * @return the program status + * @param programId the id of the program for which the status call is made + * @return the status of the program + * @throws NotFoundException if the application to which this program belongs was not found */ - @VisibleForTesting - static ProgramStatus getProgramStatus(Collection runRecords) { - boolean hasStarting = false; - for (RunRecordDetail runRecord : runRecords) { - ProgramRunStatus runStatus = runRecord.getStatus(); - if (runStatus == ProgramRunStatus.RUNNING || runStatus == ProgramRunStatus.SUSPENDED - || runStatus == ProgramRunStatus.STOPPING) { - return ProgramStatus.RUNNING; - } - hasStarting = hasStarting || runStatus == ProgramRunStatus.STARTING - || runStatus == ProgramRunStatus.PENDING; + public ProgramStatus getProgramStatus(ProgramId programId) throws Exception { + // check that app exists + ApplicationId appId = programId.getParent(); + ApplicationSpecification appSpec = store.getApplication(appId); + if (appSpec == null) { + throw new NotFoundException(appId); } - return hasStarting ? ProgramStatus.STARTING : ProgramStatus.STOPPED; + + return getExistingAppProgramStatus(appSpec, programId); } /** @@ -243,8 +214,7 @@ static ProgramStatus getProgramStatus(Collection runRecords) { * @return a {@link Map} from the {@link ProgramId} to the corresponding status; there will be no * entry for programs that do not exist. */ - public Map getProgramStatuses(Collection programRefs) - throws Exception { + public Map getProgramStatuses(Collection programRefs) { // filter the result Set visibleEntities = accessEnforcer.isVisible( new LinkedHashSet<>(programRefs), @@ -506,27 +476,6 @@ private void addProgramHistory(List histories, List overrides, boolean deb } authorizePipelineRuntimeImpersonation(userArgs); - return runInternal(programId, userArgs, sysArgs, debug); } @@ -728,7 +645,6 @@ public RunId runInternal(ProgramId programId, Map userArgs, userId = userId == null ? "" : userId; checkCapability(programDescriptor); - ProgramRunId programRunId = programId.run(runId); FlowControlService.Counter counter = flowControlService.addRequestAndGetCounter( programRunId, programOptions, programDescriptor); @@ -822,7 +738,11 @@ ProgramOptions createProgramOptions(ProgramId programId, Map use /** * Starts a Program with the specified argument overrides, skipping cluster lifecycle steps in the - * run. NOTE: This method should only be called from preview runner. + * run. + * + * NOTE: {@Link ProgramRuntimeService#run} needs be called to start the program run. + * + * NOTE: This method should only be called from preview runner. * * @param programId the {@link ProgramId} to start/stop * @param overrides the arguments to override in the program's configured user arguments @@ -831,7 +751,7 @@ ProgramOptions createProgramOptions(ProgramId programId, Map use * otherwise * @param isPreview true if the program is for preview run, for preview run, the app is * already deployed with resolved properties, so no need to regenerate app spec again - * @return {@link ProgramController} + * * @throws ConflictException if the specified program is already running, and if concurrent * runs are not allowed * @throws NotFoundException if the specified program or the app it belongs to is not found in @@ -842,7 +762,7 @@ ProgramOptions createProgramOptions(ProgramId programId, Map use * @throws Exception if there were other exceptions checking if the current user is authorized * to start the program */ - public ProgramController start(ProgramId programId, Map overrides, boolean debug, + public ProgramStartRequest prepareStart(ProgramId programId, Map overrides, boolean debug, boolean isPreview) throws Exception { accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), ApplicationPermission.EXECUTE); @@ -859,62 +779,16 @@ public ProgramController start(ProgramId programId, Map override } authorizePipelineRuntimeImpersonation(userArgs); - BasicArguments systemArguments = new BasicArguments(sysArgs); BasicArguments userArguments = new BasicArguments(userArgs); - ProgramOptions options = new SimpleProgramOptions(programId, systemArguments, userArguments, - debug); - ProgramDescriptor programDescriptor = store.loadProgram(programId); - ProgramRunId programRunId = programId.run(RunIds.generate()); + ProgramOptions programOptions = new SimpleProgramOptions(programId, systemArguments, userArguments, debug); + ProgramDescriptor programDescriptor = store.loadProgram(programId); checkCapability(programDescriptor); + ProgramRunId programRunId = programId.run(RunIds.generate()); - programStateWriter.start(programRunId, options, null, programDescriptor); - return startInternal(programDescriptor, options, programRunId); - } - - private void checkCapability(ProgramDescriptor programDescriptor) throws Exception { - //check for capability at application class level - Set applicationClasses = artifactRepository - .getArtifact(Id.Artifact.fromEntityId(programDescriptor.getArtifactId())).getMeta() - .getClasses() - .getApps(); - for (ApplicationClass applicationClass : applicationClasses) { - Set capabilities = applicationClass.getRequirements().getCapabilities(); - capabilityReader.checkAllEnabled(capabilities); - } - for (Map.Entry pluginEntry : programDescriptor.getApplicationSpecification() - .getPlugins() - .entrySet()) { - Set capabilities = pluginEntry.getValue().getPluginClass().getRequirements() - .getCapabilities(); - capabilityReader.checkAllEnabled(capabilities); - } - } - - /** - * Starts a Program run with the given arguments. This method skips cluster lifecycle steps and - * does not perform authorization checks. If the program is already started, returns the - * controller for the program. NOTE: This method should only be used from this service and the - * {@link ProgramNotificationSubscriberService} upon receiving a {@link - * ProgramRunClusterStatus#PROVISIONED} state. - * - * @param programDescriptor descriptor of the program to run - * @param programOptions options for the program run - * @param programRunId program run id - * @return controller for the program - */ - ProgramController startInternal(ProgramDescriptor programDescriptor, - ProgramOptions programOptions, ProgramRunId programRunId) { - RunId runId = RunIds.fromString(programRunId.getRun()); - - synchronized (this) { - RuntimeInfo runtimeInfo = runtimeService.lookup(programRunId.getParent(), runId); - if (runtimeInfo != null) { - return runtimeInfo.getController(); - } - return runtimeService.run(programDescriptor, programOptions, runId).getController(); - } + programStateWriter.start(programRunId, programOptions, null, programDescriptor); + return new ProgramStartRequest(programOptions, programDescriptor, programRunId); } /** @@ -1137,63 +1011,6 @@ public Map getRuntimeArgs(@Name("programId") ProgramId programId return preferencesService.getProperties(programId); } - /** - * Update log levels for the given program. Only supported program types for this action are - * {@link ProgramType#SERVICE} and {@link ProgramType#WORKER}. - * - * @param programId the {@link ProgramId} of the program for which log levels are to be - * updated - * @param logLevels the {@link Map} of the log levels to be updated. - * @param runId the run id of the program. - * @throws InterruptedException if there is an error while asynchronously updating log - * levels. - * @throws ExecutionException if there is an error while asynchronously updating log levels. - * @throws BadRequestException if the log level is not valid or the program type is not - * supported. - * @throws UnauthorizedException if the user does not have privileges to update log levels for - * the specified program. To update log levels for a program, a user needs {@link - * StandardPermission#UPDATE} on the program. - */ - public void updateProgramLogLevels(ProgramId programId, Map logLevels, - @Nullable String runId) throws Exception { - accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), - StandardPermission.UPDATE); - if (!EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programId.getType())) { - throw new BadRequestException( - String.format("Updating log levels for program type %s is not supported", - programId.getType().getPrettyName())); - } - updateLogLevels(programId, logLevels, runId); - } - - /** - * Reset log levels for the given program. Only supported program types for this action are {@link - * ProgramType#SERVICE} and {@link ProgramType#WORKER}. - * - * @param programId the {@link ProgramId} of the program for which log levels are to be - * reset. - * @param loggerNames the {@link String} set of the logger names to be updated, empty means - * reset for all loggers. - * @param runId the run id of the program. - * @throws InterruptedException if there is an error while asynchronously resetting log - * levels. - * @throws ExecutionException if there is an error while asynchronously resetting log levels. - * @throws UnauthorizedException if the user does not have privileges to reset log levels for - * the specified program. To reset log levels for a program, a user needs {@link - * StandardPermission#UPDATE} on the program. - */ - public void resetProgramLogLevels(ProgramId programId, Set loggerNames, - @Nullable String runId) throws Exception { - accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), - StandardPermission.UPDATE); - if (!EnumSet.of(ProgramType.SERVICE, ProgramType.WORKER).contains(programId.getType())) { - throw new BadRequestException( - String.format("Resetting log levels for program type %s is not supported", - programId.getType().getPrettyName())); - } - resetLogLevels(programId, loggerNames, runId); - } - /** * Ensures the caller is authorized to check if the given program exists. */ @@ -1225,10 +1042,6 @@ public void ensureLatestProgramExists(ProgramReference programRef) throws Except Store.ensureProgramExists(programRef.id(appSpec.getAppVersion()), appSpec); } - private boolean isStopped(ProgramId programId) throws Exception { - return ProgramStatus.STOPPED == getProgramStatus(programId); - } - /** * Checks if the given program is running and is allowed for concurrent execution. * @@ -1237,9 +1050,9 @@ private boolean isStopped(ProgramId programId) throws Exception { * @throws NotFoundException if the program is not found * @throws Exception if failed to determine the state */ - private synchronized void checkConcurrentExecution(ProgramId programId) throws Exception { + public void checkConcurrentExecution(ProgramId programId) throws Exception { + Map runs = store.getActiveRuns(programId); if (isConcurrentRunsInSameAppForbidden(programId.getType())) { - Map runs = runtimeService.list(programId); if (!runs.isEmpty() || !isStoppedInSameProgram(programId)) { throw new ConflictException( String.format( @@ -1248,13 +1061,8 @@ private synchronized void checkConcurrentExecution(ProgramId programId) throws E } } if (!isConcurrentRunsAllowed(programId.getType())) { - List runIds = new ArrayList<>(); - for (Map.Entry entry : runtimeService.list(programId.getType()) - .entrySet()) { - if (programId.equals(entry.getValue().getProgramId())) { - runIds.add(entry.getKey()); - } - } + List runIds = runs.keySet().stream().map(r -> programId.run(r.getRun())) + .collect(Collectors.toList()); if (!runIds.isEmpty() || !isStopped(programId)) { throw new ConflictException( String.format("Program %s is already running with run ids %s", programId, runIds)); @@ -1262,101 +1070,6 @@ private synchronized void checkConcurrentExecution(ProgramId programId) throws E } } - /** - * Returns whether the given program is stopped in all versions of the app. - * - * @param programId the id of the program for which the stopped status in all versions of the - * app is found - * @return whether the given program is stopped in all versions of the app - * @throws NotFoundException if the application to which this program belongs was not found - */ - private boolean isStoppedInSameProgram(ProgramId programId) throws Exception { - // check that app exists - Collection appIds = store.getAllAppVersionsAppIds( - programId.getParent().getAppReference()); - if (appIds == null || appIds.isEmpty()) { - throw new NotFoundException( - Id.Application.from(programId.getNamespace(), programId.getApplication())); - } - ApplicationSpecification appSpec = store.getApplication(programId.getParent()); - for (ApplicationId appId : appIds) { - ProgramId pId = appId.program(programId.getType(), programId.getProgram()); - if (!getExistingAppProgramStatus(appSpec, pId).equals(ProgramStatus.STOPPED)) { - return false; - } - } - return true; - } - - private boolean isConcurrentRunsInSameAppForbidden(ProgramType type) { - // Concurrent runs in different (or same) versions of an application are forbidden for worker - return EnumSet.of(ProgramType.WORKER).contains(type); - } - - private boolean isConcurrentRunsAllowed(ProgramType type) { - // Concurrent runs are only allowed for the Workflow, MapReduce and Spark - return EnumSet.of(ProgramType.WORKFLOW, ProgramType.MAPREDUCE, ProgramType.SPARK) - .contains(type); - } - - private Map findRuntimeInfo( - ProgramId programId, @Nullable String runId) throws BadRequestException { - - if (runId != null) { - RunId run; - try { - run = RunIds.fromString(runId); - } catch (IllegalArgumentException e) { - throw new BadRequestException("Error parsing run-id.", e); - } - ProgramRuntimeService.RuntimeInfo runtimeInfo = runtimeService.lookup(programId, run); - return runtimeInfo == null ? Collections.emptyMap() - : Collections.singletonMap(run, runtimeInfo); - } - return new HashMap<>(runtimeService.list(programId)); - } - - @Nullable - private ProgramRuntimeService.RuntimeInfo findRuntimeInfo(ProgramId programId) - throws BadRequestException { - return findRuntimeInfo(programId, null).values().stream().findFirst().orElse(null); - } - - /** - * Set instances for the given program. Only supported program types for this action are {@link - * ProgramType#SERVICE} and {@link ProgramType#WORKER}. - * - * @param programId the {@link ProgramId} of the program for which instances are to be - * updated - * @param instances the number of instances to be updated. - * @throws InterruptedException if there is an error while asynchronously updating instances - * @throws ExecutionException if there is an error while asynchronously updating instances - * @throws BadRequestException if the number of instances specified is less than 0 - * @throws UnauthorizedException if the user does not have privileges to set instances for the - * specified program. To set instances for a program, a user needs {@link - * StandardPermission#UPDATE} on the program. - */ - public void setInstances(ProgramId programId, int instances) throws Exception { - accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), - StandardPermission.UPDATE); - if (instances < 1) { - throw new BadRequestException( - String.format("Instance count should be greater than 0. Got %s.", instances)); - } - switch (programId.getType()) { - case SERVICE: - setServiceInstances(programId, instances); - break; - case WORKER: - setWorkerInstances(programId, instances); - break; - default: - throw new BadRequestException( - String.format("Setting instances for program type %s is not supported", - programId.getType().getPrettyName())); - } - } - /** * Lists all programs with the specified program type in a namespace. If perimeter security and * authorization are enabled, only returns the programs that the current user has access to. @@ -1416,76 +1129,6 @@ private boolean hasAccess(ProgramId programId) { return !accessEnforcer.isVisible(Collections.singleton(programId), principal).isEmpty(); } - private void setWorkerInstances(ProgramId programId, int instances) - throws ExecutionException, InterruptedException, BadRequestException { - int oldInstances = store.getWorkerInstances(programId); - if (oldInstances != instances) { - store.setWorkerInstances(programId, instances); - ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId); - if (runtimeInfo != null) { - runtimeInfo.getController().command(ProgramOptionConstants.INSTANCES, - ImmutableMap.of("runnable", programId.getProgram(), - "newInstances", String.valueOf(instances), - "oldInstances", String.valueOf(oldInstances))).get(); - } - } - } - - private void setServiceInstances(ProgramId programId, int instances) - throws ExecutionException, InterruptedException, BadRequestException { - int oldInstances = store.getServiceInstances(programId); - if (oldInstances != instances) { - store.setServiceInstances(programId, instances); - ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId); - if (runtimeInfo != null) { - runtimeInfo.getController().command(ProgramOptionConstants.INSTANCES, - ImmutableMap.of("runnable", programId.getProgram(), - "newInstances", String.valueOf(instances), - "oldInstances", String.valueOf(oldInstances))).get(); - } - } - } - - /** - * Helper method to update log levels for Worker or Service. - */ - private void updateLogLevels(ProgramId programId, Map logLevels, - @Nullable String runId) throws Exception { - ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId, runId).values() - .stream() - .findFirst().orElse(null); - if (runtimeInfo != null) { - LogLevelUpdater logLevelUpdater = getLogLevelUpdater(runtimeInfo); - logLevelUpdater.updateLogLevels(logLevels, null); - } - } - - /** - * Helper method to reset log levels for Worker or Service. - */ - private void resetLogLevels(ProgramId programId, Set loggerNames, @Nullable String runId) - throws Exception { - ProgramRuntimeService.RuntimeInfo runtimeInfo = findRuntimeInfo(programId, runId).values() - .stream() - .findFirst().orElse(null); - if (runtimeInfo != null) { - LogLevelUpdater logLevelUpdater = getLogLevelUpdater(runtimeInfo); - logLevelUpdater.resetLogLevels(loggerNames, null); - } - } - - /** - * Helper method to get the {@link LogLevelUpdater} for the program. - */ - private LogLevelUpdater getLogLevelUpdater(RuntimeInfo runtimeInfo) throws Exception { - ProgramController programController = runtimeInfo.getController(); - if (!(programController instanceof LogLevelUpdater)) { - throw new BadRequestException( - "Update log levels at runtime is only supported in distributed mode"); - } - return ((LogLevelUpdater) programController); - } - /** * Returns the active run records (PENDING / STARTING / RUNNING / SUSPENDED) based on the given * program id and an optional run id. @@ -1536,22 +1179,6 @@ private ProgramSpecification getLatestProgramSpecificationWithoutAuthz( return getExistingAppProgramSpecification(appMeta.getSpec(), programReference); } - /** - * Adds {@link Constants#APP_CDAP_VERSION} system argument to the argument map if known. - * - * @param programId program that corresponds to application with version information - * @param systemArgs map to add version information to - */ - public void addAppCdapVersion(ProgramId programId, Map systemArgs) { - ApplicationSpecification appSpec = store.getApplication(programId.getParent()); - if (appSpec != null) { - String appCDAPVersion = appSpec.getAppCDAPVersion(); - if (appCDAPVersion != null) { - systemArgs.put(Constants.APP_CDAP_VERSION, appCDAPVersion); - } - } - } - private Set getPluginRequirements(ProgramSpecification programSpecification) { return programSpecification.getPlugins().values() .stream().map(plugin -> new PluginRequirement(plugin.getPluginClass().getName(), @@ -1560,19 +1187,6 @@ private Set getPluginRequirements(ProgramSpecification progra .collect(Collectors.toSet()); } - private void authorizePipelineRuntimeImpersonation(Map userArgs) - throws Exception { - if ((userArgs.containsKey(SystemArguments.RUNTIME_PRINCIPAL_NAME)) - && (userArgs.containsKey(SystemArguments.RUNTIME_KEYTAB_PATH))) { - String principal = userArgs.get(SystemArguments.RUNTIME_PRINCIPAL_NAME); - LOG.debug("Checking authorisation for user: {}, using runtime config principal: {}", - authenticationContext.getPrincipal(), principal); - KerberosPrincipalId kid = new KerberosPrincipalId(principal); - accessEnforcer.enforce(kid, authenticationContext.getPrincipal(), - AccessPermission.IMPERSONATE); - } - } - private String decodeUserId(@Nullable String encodedUserId) { if (encodedUserId == null) { return ""; @@ -1612,4 +1226,168 @@ private ProgramId getLatestProgramId(ProgramReference programReference) ApplicationId applicationId = getLatestApplicationId(programReference.getParent()); return applicationId.program(programReference.getType(), programReference.getProgram()); } + + public void checkCapability(ProgramDescriptor programDescriptor) throws Exception { + // Check for capability at application class level. + Set applicationClasses = artifactRepository + .getArtifact(Id.Artifact.fromEntityId(programDescriptor.getArtifactId())).getMeta() + .getClasses() + .getApps(); + for (ApplicationClass applicationClass : applicationClasses) { + Set capabilities = applicationClass.getRequirements().getCapabilities(); + capabilityReader.checkAllEnabled(capabilities); + } + for (Map.Entry pluginEntry : programDescriptor.getApplicationSpecification() + .getPlugins() + .entrySet()) { + Set capabilities = pluginEntry.getValue().getPluginClass().getRequirements() + .getCapabilities(); + capabilityReader.checkAllEnabled(capabilities); + } + } + + /** + * Adds {@link Constants#APP_CDAP_VERSION} system argument to the argument map if known. + * + * @param programId program that corresponds to application with version information + * @param systemArgs map to add version information to + */ + private void addAppCdapVersion(ProgramId programId, Map systemArgs) { + ApplicationSpecification appSpec = store.getApplication(programId.getParent()); + if (appSpec != null) { + String appCDAPVersion = appSpec.getAppCDAPVersion(); + if (appCDAPVersion != null) { + systemArgs.put(Constants.APP_CDAP_VERSION, appCDAPVersion); + } + } + } + + private void authorizePipelineRuntimeImpersonation(Map userArgs) { + if ((userArgs.containsKey(SystemArguments.RUNTIME_PRINCIPAL_NAME)) + && (userArgs.containsKey(SystemArguments.RUNTIME_KEYTAB_PATH))) { + String principal = userArgs.get(SystemArguments.RUNTIME_PRINCIPAL_NAME); + LOG.debug("Checking authorisation for user: {}, using runtime config principal: {}", + authenticationContext.getPrincipal(), principal); + KerberosPrincipalId kid = new KerberosPrincipalId(principal); + accessEnforcer.enforce(kid, authenticationContext.getPrincipal(), + AccessPermission.IMPERSONATE); + } + } + + private static boolean isConcurrentRunsInSameAppForbidden(ProgramType type) { + // Concurrent runs in different (or same) versions of an application are forbidden for worker + return EnumSet.of(ProgramType.WORKER).contains(type); + } + + private static boolean isConcurrentRunsAllowed(ProgramType type) { + // Concurrent runs are only allowed for the Workflow, MapReduce and Spark + return EnumSet.of(ProgramType.WORKFLOW, ProgramType.MAPREDUCE, ProgramType.SPARK) + .contains(type); + } + + private boolean isStopped(ProgramId programId) throws Exception { + return ProgramStatus.STOPPED == getProgramStatus(programId); + } + + /** + * Returns whether the given program is stopped in all versions of the app. + * + * @param programId the id of the program for which the stopped status in all versions of the + * app is found + * @return whether the given program is stopped in all versions of the app + * @throws NotFoundException if the application to which this program belongs was not found + */ + private boolean isStoppedInSameProgram(ProgramId programId) throws Exception { + // check that app exists + Collection appIds = store.getAllAppVersionsAppIds( + programId.getParent().getAppReference()); + if (appIds == null || appIds.isEmpty()) { + throw new NotFoundException( + Id.Application.from(programId.getNamespace(), programId.getApplication())); + } + ApplicationSpecification appSpec = store.getApplication(programId.getParent()); + for (ApplicationId appId : appIds) { + ProgramId pId = appId.program(programId.getType(), programId.getProgram()); + if (!getExistingAppProgramStatus(appSpec, pId).equals(ProgramStatus.STOPPED)) { + return false; + } + } + return true; + } + + /** + * Returns the program status with no need of application existence check. + * + * @param appSpec the ApplicationSpecification of the existing application + * @param programId the id of the program for which the status call is made + * @return the status of the program + * @throws NotFoundException if the application to which this program belongs was not found + */ + private ProgramStatus getExistingAppProgramStatus(ApplicationSpecification appSpec, + ProgramId programId) throws Exception { + // TODO(CDAP-21126): Review access enforcement in this auxiliary function. + accessEnforcer.enforce(programId, authenticationContext.getPrincipal(), StandardPermission.GET); + ProgramSpecification spec = getExistingAppProgramSpecification(appSpec, + programId.getProgramReference()); + if (spec == null) { + // program doesn't exist + throw new NotFoundException(programId); + } + + return getProgramStatus(store.getActiveRuns(programId).values()); + } + + /** + * Returns the program status based on the active run records of a program. A program is RUNNING + * if there are any RUNNING, STOPPING, or SUSPENDED run records. A program is starting if there + * are any PENDING or STARTING run records and no RUNNING run records. Otherwise, it is STOPPED. + * + * @param runRecords run records for the program + * @return the program status + */ + @VisibleForTesting + static ProgramStatus getProgramStatus(Collection runRecords) { + boolean hasStarting = false; + for (RunRecordDetail runRecord : runRecords) { + ProgramRunStatus runStatus = runRecord.getStatus(); + if (runStatus == ProgramRunStatus.RUNNING || runStatus == ProgramRunStatus.SUSPENDED + || runStatus == ProgramRunStatus.STOPPING) { + return ProgramStatus.RUNNING; + } + hasStarting = hasStarting || runStatus == ProgramRunStatus.STARTING + || runStatus == ProgramRunStatus.PENDING; + } + return hasStarting ? ProgramStatus.STARTING : ProgramStatus.STOPPED; + } + + /** + * Returns the {@link ProgramSpecification} for the specified {@link ProgramId program}. + * + * @param appSpec the {@link ApplicationSpecification} of the existing application + * @param programReference the {@link ProgramReference program} for which the {@link + * ProgramSpecification} is requested + * @return the {@link ProgramSpecification} for the specified {@link ProgramId program}, or {@code + * null} if it does not exist + */ + @Nullable + private ProgramSpecification getExistingAppProgramSpecification(ApplicationSpecification appSpec, + ProgramReference programReference) { + String programName = programReference.getProgram(); + ProgramType type = programReference.getType(); + ProgramSpecification programSpec; + if (type == ProgramType.MAPREDUCE && appSpec.getMapReduce().containsKey(programName)) { + programSpec = appSpec.getMapReduce().get(programName); + } else if (type == ProgramType.SPARK && appSpec.getSpark().containsKey(programName)) { + programSpec = appSpec.getSpark().get(programName); + } else if (type == ProgramType.WORKFLOW && appSpec.getWorkflows().containsKey(programName)) { + programSpec = appSpec.getWorkflows().get(programName); + } else if (type == ProgramType.SERVICE && appSpec.getServices().containsKey(programName)) { + programSpec = appSpec.getServices().get(programName); + } else if (type == ProgramType.WORKER && appSpec.getWorkers().containsKey(programName)) { + programSpec = appSpec.getWorkers().get(programName); + } else { + programSpec = null; + } + return programSpec; + } } diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberService.java index a077d6d0cfc9..4b5b377fa234 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberService.java @@ -33,6 +33,7 @@ import io.cdap.cdap.api.workflow.WorkflowSpecification; import io.cdap.cdap.app.program.ProgramDescriptor; import io.cdap.cdap.app.runtime.ProgramOptions; +import io.cdap.cdap.app.runtime.ProgramRuntimeService; import io.cdap.cdap.app.runtime.ProgramStateWriter; import io.cdap.cdap.app.store.Store; import io.cdap.cdap.common.app.RunIds; @@ -91,6 +92,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import javax.annotation.Nullable; +import org.apache.twill.api.RunId; import org.apache.twill.internal.CompositeService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -108,6 +110,7 @@ public class ProgramNotificationSubscriberService extends AbstractIdleService { private final MetricsCollectionService metricsCollectionService; private final ProvisionerNotifier provisionerNotifier; private final ProgramLifecycleService programLifecycleService; + private final ProgramRuntimeService runtimeService; private final ProvisioningService provisioningService; private final ProgramStateWriter programStateWriter; private final TransactionRunner transactionRunner; @@ -123,6 +126,7 @@ public class ProgramNotificationSubscriberService extends AbstractIdleService { MetricsCollectionService metricsCollectionService, ProvisionerNotifier provisionerNotifier, ProgramLifecycleService programLifecycleService, + ProgramRuntimeService runtimeService, ProvisioningService provisioningService, ProgramStateWriter programStateWriter, TransactionRunner transactionRunner, @@ -134,6 +138,7 @@ public class ProgramNotificationSubscriberService extends AbstractIdleService { this.metricsCollectionService = metricsCollectionService; this.provisionerNotifier = provisionerNotifier; this.programLifecycleService = programLifecycleService; + this.runtimeService = runtimeService; this.provisioningService = provisioningService; this.programStateWriter = programStateWriter; this.transactionRunner = transactionRunner; @@ -229,6 +234,7 @@ private ProgramNotificationSingleTopicSubscriberService createChildService( metricsCollectionService, provisionerNotifier, programLifecycleService, + runtimeService, provisioningService, programStateWriter, transactionRunner, @@ -268,6 +274,7 @@ class ProgramNotificationSingleTopicSubscriberService private final String recordedProgramStatusPublishTopic; private final ProvisionerNotifier provisionerNotifier; private final ProgramLifecycleService programLifecycleService; + private final ProgramRuntimeService runtimeService; private final ProvisioningService provisioningService; private final ProgramStateWriter programStateWriter; private final Queue tasks; @@ -282,6 +289,7 @@ class ProgramNotificationSingleTopicSubscriberService MetricsCollectionService metricsCollectionService, ProvisionerNotifier provisionerNotifier, ProgramLifecycleService programLifecycleService, + ProgramRuntimeService runtimeService, ProvisioningService provisioningService, ProgramStateWriter programStateWriter, TransactionRunner transactionRunner, @@ -303,6 +311,7 @@ class ProgramNotificationSingleTopicSubscriberService cConf.get(Constants.AppFabric.PROGRAM_STATUS_RECORD_EVENT_TOPIC); this.provisionerNotifier = provisionerNotifier; this.programLifecycleService = programLifecycleService; + this.runtimeService = runtimeService; this.provisioningService = provisioningService; this.programStateWriter = programStateWriter; this.tasks = new LinkedList<>(); @@ -543,7 +552,8 @@ private void handleProgramEvent( SecurityRequestContext.setUserId( prgOptions.getArguments().getOption(ProgramOptionConstants.USER_ID)); try { - programLifecycleService.startInternal(prgDescriptor, prgOptions, programRunId); + RunId runId = RunIds.fromString(programRunId.getRun()); + runtimeService.run(prgDescriptor, prgOptions, runId); } catch (Exception e) { LOG.error("Failed to start program {}", programRunId, e); programStateWriter.error(programRunId, e); diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SystemProgramManagementService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SystemProgramManagementService.java index 4757c066b420..da79722933e6 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SystemProgramManagementService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/SystemProgramManagementService.java @@ -25,6 +25,7 @@ import io.cdap.cdap.common.conf.Constants; import io.cdap.cdap.common.service.AbstractRetryableScheduledService; import io.cdap.cdap.common.service.RetryStrategies; +import io.cdap.cdap.internal.app.runtime.ProgramStartRequest; import io.cdap.cdap.proto.ProgramType; import io.cdap.cdap.proto.id.NamespaceId; import io.cdap.cdap.proto.id.ProgramId; @@ -121,7 +122,10 @@ private void startPrograms(Map enabledProgramsMap) { Map overrides = enabledProgramsMap.get(programId).asMap(); LOG.debug("Starting program {} with args {}", programId, overrides); try { - programLifecycleService.start(programId, overrides, false, false); + ProgramStartRequest startRequest = programLifecycleService.prepareStart(programId, overrides, false, false); + programRuntimeService.run( + startRequest.getProgramDescriptor(), startRequest.getProgramOptions(), startRequest.getRunId()) + .getController(); } catch (ConflictException ex) { // Ignore if the program is already running. LOG.debug("Program {} is already running.", programId); diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java index e0b59e4a2faa..f790165316fd 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/DefaultStore.java @@ -39,6 +39,7 @@ import io.cdap.cdap.app.store.ScanApplicationsRequest; import io.cdap.cdap.app.store.Store; import io.cdap.cdap.common.ApplicationNotFoundException; +import io.cdap.cdap.common.BadRequestException; import io.cdap.cdap.common.ConflictException; import io.cdap.cdap.common.NotFoundException; import io.cdap.cdap.common.ProgramNotFoundException; @@ -939,6 +940,18 @@ public ApplicationMeta getLatest(ApplicationReference appRef) { }); } + @Override + public ApplicationId getLatestApp(ApplicationReference appRef) throws ApplicationNotFoundException { + ApplicationMeta appMeta = getLatest(appRef); + if (appMeta == null) { + throw new ApplicationNotFoundException(appRef); + } + + return new ApplicationId(appRef.getNamespace(), + appRef.getApplication(), + appMeta.getSpec().getAppVersion()); + } + @Override public Collection getAllAppVersionsAppIds(ApplicationReference appRef) { return TransactionRunners.run(transactionRunner, context -> { diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/capability/CapabilityManagementService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/capability/CapabilityManagementService.java index c8d5645345ad..f4df5e36e2f4 100644 --- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/capability/CapabilityManagementService.java +++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/capability/CapabilityManagementService.java @@ -24,7 +24,6 @@ import io.cdap.cdap.common.service.AbstractRetryableScheduledService; import io.cdap.cdap.common.service.RetryStrategies; import io.cdap.cdap.common.utils.DirUtils; -import io.cdap.cdap.internal.app.services.SystemProgramManagementService; import java.io.File; import java.io.FileReader; import java.io.Reader; @@ -46,8 +45,7 @@ public class CapabilityManagementService extends AbstractRetryableScheduledServi private final CapabilityApplier capabilityApplier; @Inject - CapabilityManagementService(CConfiguration cConf, CapabilityApplier capabilityApplier, - SystemProgramManagementService systemProgramManagementService) { + CapabilityManagementService(CConfiguration cConf, CapabilityApplier capabilityApplier) { super(RetryStrategies .fixDelay(cConf.getLong(Constants.Capability.DIR_SCAN_INTERVAL_MINUTES), TimeUnit.MINUTES)); this.cConf = cConf; diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeServiceTest.java index 1c6cd896fe6b..4b1922a33ca6 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeServiceTest.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/app/runtime/AbstractProgramRuntimeServiceTest.java @@ -287,7 +287,8 @@ public void testTetheredRun() throws IOException, ExecutionException, Interrupte InMemoryProgramRunDispatcher launchDispatcher = new TestProgramRunDispatcher(cConf, runnerFactory, program, locationFactory, remoteClientFactory, true); - ProgramRuntimeService runtimeService = new TestProgramRuntimeService(cConf, runnerFactory, null, launchDispatcher); + ProgramRuntimeService runtimeService = new TestProgramRuntimeService(cConf, runnerFactory, null, + launchDispatcher); runtimeService.startAndWait(); try { ProgramDescriptor descriptor = new ProgramDescriptor(program.getId(), null, diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/AppFabricClient.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/AppFabricClient.java index 2c4e0fde2c8a..cd2af980e5d0 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/AppFabricClient.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/AppFabricClient.java @@ -33,6 +33,7 @@ import io.cdap.cdap.gateway.handlers.AppLifecycleHttpHandler; import io.cdap.cdap.gateway.handlers.NamespaceHttpHandler; import io.cdap.cdap.gateway.handlers.ProgramLifecycleHttpHandler; +import io.cdap.cdap.gateway.handlers.ProgramRuntimeHttpHandler; import io.cdap.cdap.gateway.handlers.WorkflowHttpHandler; import io.cdap.cdap.gateway.handlers.util.AbstractAppFabricHttpHandler; import io.cdap.cdap.internal.app.BufferFileInputStream; @@ -106,6 +107,7 @@ public class AppFabricClient { private final LocationFactory locationFactory; private final AppLifecycleHttpHandler appLifecycleHttpHandler; private final ProgramLifecycleHttpHandler programLifecycleHttpHandler; + private final ProgramRuntimeHttpHandler programRuntimeHttpHandler; private final WorkflowHttpHandler workflowHttpHandler; private final NamespaceHttpHandler namespaceHttpHandler; private final NamespaceQueryAdmin namespaceQueryAdmin; @@ -114,12 +116,14 @@ public class AppFabricClient { public AppFabricClient(LocationFactory locationFactory, AppLifecycleHttpHandler appLifecycleHttpHandler, ProgramLifecycleHttpHandler programLifecycleHttpHandler, + ProgramRuntimeHttpHandler programRuntimeHttpHandler, NamespaceHttpHandler namespaceHttpHandler, NamespaceQueryAdmin namespaceQueryAdmin, WorkflowHttpHandler workflowHttpHandler) { this.locationFactory = locationFactory; this.appLifecycleHttpHandler = appLifecycleHttpHandler; this.programLifecycleHttpHandler = programLifecycleHttpHandler; + this.programRuntimeHttpHandler = programRuntimeHttpHandler; this.namespaceHttpHandler = namespaceHttpHandler; this.namespaceQueryAdmin = namespaceQueryAdmin; this.workflowHttpHandler = workflowHttpHandler; @@ -241,7 +245,7 @@ public void setWorkerInstances(String namespaceId, String appId, String workerId json.addProperty("instances", instances); request.content().writeCharSequence(json.toString(), StandardCharsets.UTF_8); HttpUtil.setContentLength(request, request.content().readableBytes()); - programLifecycleHttpHandler.setWorkerInstances(request, responder, namespaceId, appId, workerId); + programRuntimeHttpHandler.setWorkerInstances(request, responder, namespaceId, appId, workerId); verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Set worker instances failed"); } @@ -249,7 +253,7 @@ public Instances getWorkerInstances(String namespaceId, String appId, String wor MockResponder responder = new MockResponder(); String uri = String.format("%s/apps/%s/worker/%s/instances", getNamespacePath(namespaceId), appId, workerId); HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); - programLifecycleHttpHandler.getWorkerInstances(request, responder, namespaceId, appId, workerId); + programRuntimeHttpHandler.getWorkerInstances(request, responder, namespaceId, appId, workerId); verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Get worker instances failed"); return responder.decodeResponseContent(Instances.class); } @@ -264,7 +268,7 @@ public void setServiceInstances(String namespaceId, String applicationId, String json.addProperty("instances", instances); request.content().writeCharSequence(json.toString(), StandardCharsets.UTF_8); HttpUtil.setContentLength(request, request.content().readableBytes()); - programLifecycleHttpHandler.setServiceInstances(request, responder, namespaceId, applicationId, serviceName); + programRuntimeHttpHandler.setServiceInstances(request, responder, namespaceId, applicationId, serviceName); verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Set service instances failed"); } @@ -274,7 +278,7 @@ public ServiceInstances getServiceInstances(String namespaceId, String applicati String uri = String.format("%s/apps/%s/services/%s/instances", getNamespacePath(namespaceId), applicationId, serviceName); HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri); - programLifecycleHttpHandler.getServiceInstances(request, responder, namespaceId, applicationId, serviceName); + programRuntimeHttpHandler.getServiceInstances(request, responder, namespaceId, applicationId, serviceName); verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Get service instances failed"); return responder.decodeResponseContent(ServiceInstances.class); } diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramLifecycleServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramLifecycleServiceTest.java index 546198425f35..12f5700ff401 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramLifecycleServiceTest.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramLifecycleServiceTest.java @@ -87,7 +87,7 @@ public static void shutdown() { @Test public void testEmptyRunsIsStopped() { - Assert.assertEquals(ProgramStatus.STOPPED, ProgramLifecycleService.getProgramStatus(Collections.emptyList())); + Assert.assertEquals(ProgramStatus.STOPPED, programLifecycleService.getProgramStatus(Collections.emptyList())); } @Test @@ -101,33 +101,33 @@ public void testProgramStatusFromSingleRun() { .build(); // pending or starting -> starting - ProgramStatus status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + ProgramStatus status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.STARTING, status); record = RunRecordDetail.builder(record).setStatus(ProgramRunStatus.STARTING).build(); - status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.STARTING, status); // running, suspended, resuming -> running record = RunRecordDetail.builder(record).setStatus(ProgramRunStatus.RUNNING).build(); - status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.RUNNING, status); record = RunRecordDetail.builder(record).setStatus(ProgramRunStatus.SUSPENDED).build(); - status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.RUNNING, status); // failed, killed, completed -> stopped record = RunRecordDetail.builder(record).setStatus(ProgramRunStatus.FAILED).build(); - status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.STOPPED, status); record = RunRecordDetail.builder(record).setStatus(ProgramRunStatus.KILLED).build(); - status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.STOPPED, status); record = RunRecordDetail.builder(record).setStatus(ProgramRunStatus.COMPLETED).build(); - status = ProgramLifecycleService.getProgramStatus(Collections.singleton(record)); + status = programLifecycleService.getProgramStatus(Collections.singleton(record)); Assert.assertEquals(ProgramStatus.STOPPED, status); } @@ -158,18 +158,18 @@ public void testProgramStatusFromMultipleRuns() { .setStatus(ProgramRunStatus.COMPLETED).build(); // running takes precedence over others - ProgramStatus status = ProgramLifecycleService.getProgramStatus( + ProgramStatus status = programLifecycleService.getProgramStatus( Arrays.asList(pending, starting, running, killed, failed, completed)); Assert.assertEquals(ProgramStatus.RUNNING, status); // starting takes precedence over stopped - status = ProgramLifecycleService.getProgramStatus(Arrays.asList(pending, killed, failed, completed)); + status = programLifecycleService.getProgramStatus(Arrays.asList(pending, killed, failed, completed)); Assert.assertEquals(ProgramStatus.STARTING, status); - status = ProgramLifecycleService.getProgramStatus(Arrays.asList(starting, killed, failed, completed)); + status = programLifecycleService.getProgramStatus(Arrays.asList(starting, killed, failed, completed)); Assert.assertEquals(ProgramStatus.STARTING, status); // end states are stopped - status = ProgramLifecycleService.getProgramStatus(Arrays.asList(killed, failed, completed)); + status = programLifecycleService.getProgramStatus(Arrays.asList(killed, failed, completed)); Assert.assertEquals(ProgramStatus.STOPPED, status); } diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SystemProgramManagementServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SystemProgramManagementServiceTest.java index 054de2984f41..620e3225942f 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SystemProgramManagementServiceTest.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/SystemProgramManagementServiceTest.java @@ -26,6 +26,7 @@ import io.cdap.cdap.common.test.AppJarHelper; import io.cdap.cdap.internal.AppFabricTestHelper; import io.cdap.cdap.internal.app.runtime.BasicArguments; +import io.cdap.cdap.internal.app.runtime.ProgramStartRequest; import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository; import io.cdap.cdap.internal.app.services.http.AppFabricTestBase; import io.cdap.cdap.proto.ProgramRunStatus; @@ -53,9 +54,9 @@ public class SystemProgramManagementServiceTest extends AppFabricTestBase { private static SystemProgramManagementService progmMgmtSvc; private static ProgramLifecycleService programLifecycleService; private static ApplicationLifecycleService applicationLifecycleService; + private static ProgramRuntimeService runtimeService; private static LocationFactory locationFactory; private static ArtifactRepository artifactRepository; - private static final String VERSION = "1.0.0"; private static final String APP_NAME = SystemProgramManagementTestApp.NAME; private static final String NAMESPACE = "system"; @@ -65,6 +66,7 @@ public class SystemProgramManagementServiceTest extends AppFabricTestBase { @BeforeClass public static void setup() { programLifecycleService = getInjector().getInstance(ProgramLifecycleService.class); + runtimeService = getInjector().getInstance(ProgramRuntimeService.class); applicationLifecycleService = getInjector().getInstance(ApplicationLifecycleService.class); locationFactory = getInjector().getInstance(LocationFactory.class); artifactRepository = getInjector().getInstance(ArtifactRepository.class); @@ -100,8 +102,8 @@ public void testIteration() throws Exception { Assert.assertEquals(ProgramStatus.STOPPED.name(), getProgramStatus(programId)); assertProgramRuns(programId, ProgramRunStatus.RUNNING, 0); //Run the program manually twice to test pruning. One run should be killed - programLifecycleService.start(programId, new HashMap<>(), false, false); - programLifecycleService.start(programId, new HashMap<>(), false, false); + startProgram(programId, new HashMap<>()); + startProgram(programId, new HashMap<>()); assertProgramRuns(programId, ProgramRunStatus.RUNNING, 2); progmMgmtSvc.setProgramsEnabled(enabledServices); progmMgmtSvc.runTask(); @@ -125,4 +127,12 @@ private void deployTestApp() throws Exception { // no-op }, null, false, false, false, Collections.emptyMap()); } + + private void startProgram(ProgramId programId, Map overrides) + throws Exception { + ProgramStartRequest startRequest = programLifecycleService.prepareStart( + programId, overrides, false, false); + runtimeService.run(startRequest.getProgramDescriptor(), startRequest.getProgramOptions(), startRequest.getRunId()) + .getController(); + } } diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/capability/CapabilityManagementServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/capability/CapabilityManagementServiceTest.java index 041b66a63395..2f24fb2d850a 100644 --- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/capability/CapabilityManagementServiceTest.java +++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/capability/CapabilityManagementServiceTest.java @@ -30,6 +30,8 @@ import io.cdap.cdap.api.artifact.ArtifactSummary; import io.cdap.cdap.app.program.ManifestFields; import io.cdap.cdap.app.program.ProgramDescriptor; +import io.cdap.cdap.app.runtime.ProgramRuntimeService; +import io.cdap.cdap.app.runtime.ProgramStateWriter; import io.cdap.cdap.common.conf.CConfiguration; import io.cdap.cdap.common.conf.Constants; import io.cdap.cdap.common.id.Id; @@ -38,6 +40,7 @@ import io.cdap.cdap.common.test.PluginJarHelper; import io.cdap.cdap.internal.AppFabricTestHelper; import io.cdap.cdap.internal.app.deploy.pipeline.ApplicationWithPrograms; +import io.cdap.cdap.internal.app.runtime.ProgramStartRequest; import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository; import io.cdap.cdap.internal.app.services.ApplicationLifecycleService; import io.cdap.cdap.internal.app.services.ProgramLifecycleService; @@ -82,6 +85,8 @@ public class CapabilityManagementServiceTest extends AppFabricTestBase { private static ApplicationLifecycleService applicationLifecycleService; private static ProgramLifecycleService programLifecycleService; private static CapabilityStatusStore capabilityStatusStore; + private static ProgramStateWriter programStateWriter; + private static ProgramRuntimeService runtimeService; private static final Gson GSON = new Gson(); @BeforeClass @@ -93,6 +98,8 @@ public static void setup() { capabilityStatusStore = new CapabilityStatusStore(getInjector().getInstance(TransactionRunner.class)); applicationLifecycleService = getInjector().getInstance(ApplicationLifecycleService.class); programLifecycleService = getInjector().getInstance(ProgramLifecycleService.class); + programStateWriter = getInjector().getInstance(ProgramStateWriter.class); + runtimeService = getInjector().getInstance(ProgramRuntimeService.class); capabilityManagementService.stopAndWait(); } @@ -561,7 +568,7 @@ public void testProgramStart() throws Exception { }); Iterable programs = applicationWithPrograms.getPrograms(); for (ProgramDescriptor program : programs) { - programLifecycleService.start(program.getProgramId(), new HashMap<>(), false, false); + startProgram(program.getProgramId(), new HashMap<>()); } ProgramId programId = new ProgramId(applicationId, ProgramType.WORKFLOW, CapabilitySleepingWorkflowApp.SleepWorkflow.class.getSimpleName()); @@ -588,7 +595,7 @@ public void testProgramStart() throws Exception { //try starting programs for (ProgramDescriptor program : programs) { try { - programLifecycleService.start(program.getProgramId(), new HashMap<>(), false, false); + startProgram(program.getProgramId(), new HashMap<>()); Assert.fail("expecting exception"); } catch (CapabilityNotAvailableException ex) { //expecting exception @@ -641,7 +648,7 @@ public void testProgramWithPluginStart() throws Exception { }); Iterable programs = applicationWithPrograms.getPrograms(); for (ProgramDescriptor program : programs) { - programLifecycleService.start(program.getProgramId(), new HashMap<>(), false, false); + startProgram(program.getProgramId(), new HashMap<>()); } ProgramId programId = new ProgramId(applicationId, ProgramType.WORKFLOW, CapabilitySleepingWorkflowPluginApp.SleepWorkflow.class.getSimpleName()); @@ -668,7 +675,7 @@ public void testProgramWithPluginStart() throws Exception { //try starting programs for (ProgramDescriptor program : programs) { try { - programLifecycleService.start(program.getProgramId(), new HashMap<>(), false, false); + startProgram(program.getProgramId(), new HashMap<>()); Assert.fail("expecting exception"); } catch (CapabilityNotAvailableException ex) { //expecting exception @@ -819,4 +826,12 @@ private void deployArtifactAndApp(Class applicationClass, String appName, Str null, programId -> { }); } + + private void startProgram(ProgramId programId, Map overrides) + throws Exception { + ProgramStartRequest startRequest = programLifecycleService.prepareStart( + programId, overrides, false, false); + runtimeService.run(startRequest.getProgramDescriptor(), startRequest.getProgramOptions(), startRequest.getRunId()) + .getController(); + } } diff --git a/cdap-gateway/src/main/java/io/cdap/cdap/gateway/router/RouterPathLookup.java b/cdap-gateway/src/main/java/io/cdap/cdap/gateway/router/RouterPathLookup.java index b8f08ee4e986..779c017e9f61 100644 --- a/cdap-gateway/src/main/java/io/cdap/cdap/gateway/router/RouterPathLookup.java +++ b/cdap-gateway/src/main/java/io/cdap/cdap/gateway/router/RouterPathLookup.java @@ -34,25 +34,6 @@ */ public final class RouterPathLookup extends AbstractHttpHandler { - public static final Set APP_FABRIC_PROCESSOR_PATH_PARTS = - createAppFabricPathParts(); - - private static Set createAppFabricPathParts() { - return ImmutableSet.of("previousruntime", - "nextruntime", - "status", - "stop", - "start", - "instances", - "runcount", - "runs", - "mapreduce", - "spark", - "workflows", - "services", - "workers"); - } - @SuppressWarnings("unused") private enum AllowedMethod { GET, PUT, POST, DELETE @@ -105,12 +86,6 @@ public RouteDestination getRoutingService(String requestPath, HttpRequest httpRe if (uriParts[0].equals(Constants.Gateway.API_VERSION_3_TOKEN)) { return getV3RoutingService(uriParts, requestMethod); } - - // TODO (CDAP-21112): Move HTTP handler from Appfabric processor to server after fixing - // ProgramRuntimeService and RunRecordMonitorService. - if (uriParts[0].equals(Gateway.INTERNAL_API_VERSION_3_TOKEN)) { - return getV3InternalRoutingService(uriParts); - } } catch (Exception e) { // Ignore exception. Default routing to app-fabric. } @@ -126,15 +101,6 @@ private boolean isUserServiceType(String uriPart) { return false; } - private RouteDestination getV3InternalRoutingService(String[] uriParts) { - if (beginsWith(uriParts, "v3Internal", "namespaces", null)) { - // ProgramLifecycleHttpHandlerInternal and AppLifecycleHttpHandlerInternal paths. - // Paths: "/v3Internal/namespaces/{namespace-id}/**" - return APP_FABRIC_PROCESSOR; - } - return APP_FABRIC_HTTP; - } - @Nullable private RouteDestination getV3RoutingService(String[] uriParts, AllowedMethod requestMethod) { if ((uriParts.length >= 2) && uriParts[1].equals("feeds")) { @@ -163,6 +129,21 @@ private RouteDestination getV3RoutingService(String[] uriParts, AllowedMethod re } else if (beginsWith(uriParts, "v3", "system", "services", null, "logs")) { //Log Handler Path /v3/system/services//logs return LOG_QUERY; + } else if (beginsWith(uriParts, "v3", "namespaces", null) + && (endsWith(uriParts, "instances") + || endsWith(uriParts, "live-info") + || endsWith(uriParts, "loglevels") + || endsWith(uriParts, "resetloglevels"))) { + // ProgramRuntimeLifecycleHttpHandler Paths: + // v3/namespaces/{ns-id}/instances + // v3/namespaces/{ns-id}/apps/{app-id}/services/{service-id}/instances + // v3/namespaces/{ns-id}/apps/{app-id}/workers/{worker-id}/instances + // v3/namespaces/{ns-id}/apps/{app-id}/{program-category}/{program-id}/live-info + // v3/namespaces/{ns-id}/apps/{app-name}/{program-type}/{program-name}/runs/{run-id}/loglevels + // v3/namespaces/{ns-id}/apps/{app-name}/versions/{appVer}/{progType}/{progName}/runs/{runId}/loglevels + // v3/namespaces/{ns-id}/apps/{app-name}/{program-type}/{progName}/runs/{runId}/resetloglevels + // v3/namespaces/{ns-id}/apps/{app-name}/versions/{appVer}/{progType}/{progName}/runs/{runId}/resetloglevels + return APP_FABRIC_PROCESSOR; } else if ((!beginsWith(uriParts, "v3", "namespaces", null, "securekeys")) && ( endsWith(uriParts, "metadata") || @@ -192,7 +173,7 @@ private RouteDestination getV3RoutingService(String[] uriParts, AllowedMethod re || beginsWith(uriParts, "v3", "profiles")) { return APP_FABRIC_HTTP; } else if (beginsWith(uriParts, "v3", "namespaces", null, "runs")) { - return APP_FABRIC_PROCESSOR; + return APP_FABRIC_HTTP; } else if (beginsWith(uriParts, "v3", "namespaces", null, "previews")) { return PREVIEW_HTTP; } else if (beginsWith(uriParts, "v3", "system", "serviceproviders")) { @@ -262,25 +243,6 @@ private RouteDestination getV3RoutingService(String[] uriParts, AllowedMethod re // we don't want to expose endpoints for direct metadata mutation from CDAP master // /v3/metadata-internals/{mutation-type} return DONT_ROUTE; - } else if (beginsWith(uriParts, "v3", "namespaces", null, "apps", null, "preferences") - || beginsWith(uriParts, "v3", "namespaces", null, "apps", null, null, null, "preferences")) { - // App Preferences Paths: - // /v3/namespaces/{namespace-id}/apps/{application-id}/preferences - // /v3/namespaces/{namespace-id}/apps/{application-id}/{program-type}/{program-id}/preferences - return APP_FABRIC_HTTP; - } else if (beginsWith(uriParts, "v3", "namespaces", null, "apps", null, "datasets") - || beginsWith(uriParts, "v3", "namespaces", null, "apps", null, null, null, "datasets")) { - return APP_FABRIC_HTTP; - } else if (beginsWith(uriParts, "v3", "namespaces", null, "apps") - || beginsWith(uriParts, "v3", "namespaces", null, "upgrade") - || beginsWith(uriParts, "v3", "namespaces", null, "appdetail") - || beginsWith(uriParts, "v3", "namespaces", null, "schedules")) { - // Paths for AppLifecycleHttpHandler, ProgramLifecycleHttpHandler, WorkflowHttpHandler. - return APP_FABRIC_PROCESSOR; - } else if (beginsWith(uriParts, "v3", "namespaces") - && APP_FABRIC_PROCESSOR_PATH_PARTS.contains(uriParts[3])) { - // Paths for ProgramLifecycleHttpHandler. - return APP_FABRIC_PROCESSOR; } return APP_FABRIC_HTTP; } diff --git a/cdap-gateway/src/test/java/io/cdap/cdap/gateway/router/RouterPathLookupTest.java b/cdap-gateway/src/test/java/io/cdap/cdap/gateway/router/RouterPathLookupTest.java index 0b28272422c6..90b30c9f8b35 100644 --- a/cdap-gateway/src/test/java/io/cdap/cdap/gateway/router/RouterPathLookupTest.java +++ b/cdap-gateway/src/test/java/io/cdap/cdap/gateway/router/RouterPathLookupTest.java @@ -56,7 +56,7 @@ public void testBatchRunsPath() { String path = "/v3/namespaces/n1/runs"; HttpRequest httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("POST"), path); RouteDestination result = pathLookup.getRoutingService(path, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); } @Test @@ -165,13 +165,13 @@ public void testServicePath() { HttpRequest httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("PUT"), servicePath); httpRequest.headers().set(Constants.Gateway.API_KEY, API_KEY); RouteDestination result = pathLookup.getRoutingService(servicePath, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); servicePath = "v3/namespaces/some/apps/otherAppName/services/CatalogLookup//methods////"; httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("GET"), servicePath); httpRequest.headers().set(Constants.Gateway.API_KEY, API_KEY); result = pathLookup.getRoutingService(servicePath, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); // v3 servicePaths servicePath = "/v3/namespaces/testnamespace/apps//PurchaseHistory///services/CatalogLookup///methods//ping/1"; @@ -207,13 +207,13 @@ public void testServicePath() { httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("PUT"), servicePath); httpRequest.headers().set(Constants.Gateway.API_KEY, API_KEY); result = pathLookup.getRoutingService(servicePath, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); servicePath = "v3/namespaces/testnamespace/apps/AppName/services/CatalogLookup////methods////"; httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("GET"), servicePath); httpRequest.headers().set(Constants.Gateway.API_KEY, API_KEY); result = pathLookup.getRoutingService(servicePath, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); } @Test @@ -226,7 +226,7 @@ public void testRouterServicePathLookUp() { String path = "/v3/namespaces/default//apps/ResponseCodeAnalytics/services/LogAnalyticsService/status"; HttpRequest httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("GET"), path); RouteDestination result = pathLookup.getRoutingService(path, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); } @Test @@ -234,7 +234,7 @@ public void testRouterWorkFlowPathLookUp() { String path = "/v3/namespaces/default/apps///PurchaseHistory///workflows/PurchaseHistoryWorkflow/status"; HttpRequest httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("GET"), path); RouteDestination result = pathLookup.getRoutingService(path, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); } @Test @@ -242,7 +242,7 @@ public void testRouterDeployPathLookUp() { String path = "/v3/namespaces/default//apps/"; HttpRequest httpRequest = new DefaultHttpRequest(VERSION, new HttpMethod("PUT"), path); RouteDestination result = pathLookup.getRoutingService(path, httpRequest); - Assert.assertEquals(RouterPathLookup.APP_FABRIC_PROCESSOR, result); + Assert.assertEquals(RouterPathLookup.APP_FABRIC_HTTP, result); } @Test @@ -422,23 +422,16 @@ public void testProfilePaths() { @Test public void testAppLifecycleAndWorkflowPaths() { - assertRouting("v3/namespaces/default/apps", RouterPathLookup.APP_FABRIC_PROCESSOR); - assertRouting("v3/namespaces/default/apps/myapp", RouterPathLookup.APP_FABRIC_PROCESSOR); - assertRouting("v3/namespaces/default/upgrade", RouterPathLookup.APP_FABRIC_PROCESSOR); - assertRouting("v3/namespaces/default/appdetail", RouterPathLookup.APP_FABRIC_PROCESSOR); - } - - @Test - public void testProgramLifecyclePaths() { - for (String part : RouterPathLookup.APP_FABRIC_PROCESSOR_PATH_PARTS) { - assertRouting("v3/namespaces/default/" + part, RouterPathLookup.APP_FABRIC_PROCESSOR); - } + assertRouting("v3/namespaces/default/apps", RouterPathLookup.APP_FABRIC_HTTP); + assertRouting("v3/namespaces/default/apps/myapp", RouterPathLookup.APP_FABRIC_HTTP); + assertRouting("v3/namespaces/default/upgrade", RouterPathLookup.APP_FABRIC_HTTP); + assertRouting("v3/namespaces/default/appdetail", RouterPathLookup.APP_FABRIC_HTTP); } @Test public void testProgramLifecycleInternalAndAppLifecycleInternalPaths() { assertRouting("v3Internal/namespaces//apps//versions//workflows/DataPipelineWorkflow/runs/", - RouterPathLookup.APP_FABRIC_PROCESSOR); + RouterPathLookup.APP_FABRIC_HTTP); } @Test @@ -453,6 +446,18 @@ public void testAppPreferencesPaths() { assertRouting("v3/namespaces//apps////preferences", RouterPathLookup.APP_FABRIC_HTTP); } + @Test + public void testProgramRuntimeLifecyclePaths() { + assertRouting("v3/namespaces//instances", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps//services//instances", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps//workers//instances", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps////live-info", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps////runs//loglevels", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps////runs//resetloglevels", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps//versions////runs//loglevels", RouterPathLookup.APP_FABRIC_PROCESSOR); + assertRouting("v3/namespaces//apps//versions////runs//resetloglevels", RouterPathLookup.APP_FABRIC_PROCESSOR); + } + @Test public void testBeginsWith() { // anything begins empty sequence diff --git a/cdap-watchdog/src/main/java/io/cdap/cdap/logging/gateway/handlers/RemoteProgramRunRecordFetcher.java b/cdap-watchdog/src/main/java/io/cdap/cdap/logging/gateway/handlers/RemoteProgramRunRecordFetcher.java index 001fb64595c2..1eea3a4efb79 100644 --- a/cdap-watchdog/src/main/java/io/cdap/cdap/logging/gateway/handlers/RemoteProgramRunRecordFetcher.java +++ b/cdap-watchdog/src/main/java/io/cdap/cdap/logging/gateway/handlers/RemoteProgramRunRecordFetcher.java @@ -49,7 +49,7 @@ public RemoteProgramRunRecordFetcher(RemoteClientFactory remoteClientFactory) { // ProgramRuntimeService and RunRecordMonitorService. // Use APP_FABRIC_HTTP instead of APP_FABRIC_PROCESSOR after the fix. this.remoteClient = remoteClientFactory.createRemoteClient( - Constants.Service.APP_FABRIC_PROCESSOR, + Constants.Service.APP_FABRIC_HTTP, new DefaultHttpRequestConfig(false), Constants.Gateway.INTERNAL_API_VERSION_3); }