properties = Objects.firstNonNull(scheduleFromRequest.getProperties(),
+ Collections.emptyMap());
+ List extends Constraint> constraints = Objects.firstNonNull(
+ scheduleFromRequest.getConstraints(), NO_CONSTRAINTS);
+ long timeoutMillis =
+ Objects.firstNonNull(scheduleFromRequest.getTimeoutMillis(),
+ Schedulers.JOB_QUEUE_TIMEOUT_MILLIS);
+ ProgramSchedule schedule = new ProgramSchedule(scheduleName, description, programId, properties,
+ scheduleFromRequest.getTrigger(), constraints, timeoutMillis);
+ programScheduleService.add(schedule);
+ responder.sendStatus(HttpResponseStatus.OK);
+ }
+
+ /**
+ * Updates the schedule for a given {@code appName}.
+ */
+ @POST
+ @Path("apps/{app-name}/schedules/{schedule-name}/update")
+ @AuditPolicy(AuditDetail.REQUEST_BODY)
+ public void updateSchedule(FullHttpRequest request, HttpResponder responder,
+ @PathParam("namespace-id") String namespaceId,
+ @PathParam("app-name") String appName,
+ @PathParam("schedule-name") String scheduleName) throws Exception {
+ doUpdateSchedule(request, responder, namespaceId, appName, scheduleName);
+ }
+
+ /*
+ * Deprecated : Schedules are versionless.
+ * */
+ @Deprecated
+ @POST
+ @Path("apps/{app-name}/versions/{app-version}/schedules/{schedule-name}/update")
+ @AuditPolicy(AuditDetail.REQUEST_BODY)
+ public void updateScheduleVersioned(FullHttpRequest request, HttpResponder responder,
+ @PathParam("namespace-id") String namespaceId,
+ @PathParam("app-name") String appName,
+ @PathParam("app-version") String appVersion,
+ @PathParam("schedule-name") String scheduleName) throws Exception {
+ doUpdateSchedule(request, responder, namespaceId, appName, scheduleName);
+ }
+
+ private void doUpdateSchedule(FullHttpRequest request, HttpResponder responder,
+ String namespaceId, String appId,
+ String scheduleName) throws Exception {
+
+ ScheduleId scheduleId = new ApplicationId(namespaceId, appId).schedule(scheduleName);
+ ScheduleDetail scheduleDetail = readScheduleDetailBody(request, scheduleName);
+
+ programScheduleService.update(scheduleId, scheduleDetail);
+ responder.sendStatus(HttpResponseStatus.OK);
+ }
+
+ private ScheduleDetail readScheduleDetailBody(FullHttpRequest request, String scheduleName)
+ throws BadRequestException, IOException {
+
+ JsonElement json;
+ 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 = ProgramHandlerUtil.fromJson(reader, JsonElement.class);
+ } catch (IOException e) {
+ throw new IOException("Error reading request body", e);
+ } catch (JsonSyntaxException e) {
+ throw new BadRequestException("Request body is invalid json: " + e.getMessage());
+ }
+ if (!json.isJsonObject()) {
+ throw new BadRequestException(
+ "Expected a json object in the request body but received " + ProgramHandlerUtil.toJson(json));
+ }
+ ScheduleDetail scheduleDetail;
+ try {
+ scheduleDetail = ProgramHandlerUtil.fromJson(json, ScheduleDetail.class);
+ } catch (JsonSyntaxException e) {
+ throw new BadRequestException(
+ "Error parsing request body as a schedule specification: " + e.getMessage());
+ }
+
+ // If the schedule name is present in the request body, it should match the name in path params
+ if (scheduleDetail.getName() != null && !scheduleName.equals(scheduleDetail.getName())) {
+ throw new BadRequestException(String.format(
+ "Schedule name in the body of the request (%s) does not match the schedule name in the path parameter (%s)",
+ scheduleDetail.getName(), scheduleName));
+ }
+ return scheduleDetail;
+ }
+
+ @DELETE
+ @Path("apps/{app-name}/schedules/{schedule-name}")
+ public void deleteSchedule(HttpRequest request, HttpResponder responder,
+ @PathParam("namespace-id") String namespaceId,
+ @PathParam("app-name") String appName,
+ @PathParam("schedule-name") String scheduleName) throws Exception {
+ doDeleteSchedule(responder, namespaceId, appName, scheduleName);
+ }
+
+ /*
+ * Deprecated : Schedules are versionless.
+ * */
+ @Deprecated
+ @DELETE
+ @Path("apps/{app-name}/versions/{app-version}/schedules/{schedule-name}")
+ public void deleteScheduleVersioned(HttpRequest request, HttpResponder responder,
+ @PathParam("namespace-id") String namespaceId,
+ @PathParam("app-name") String appName,
+ @PathParam("app-version") String appVersion,
+ @PathParam("schedule-name") String scheduleName) throws Exception {
+ doDeleteSchedule(responder, namespaceId, appName, scheduleName);
+ }
+
+ private void doDeleteSchedule(HttpResponder responder, String namespaceId, String appName,
+ String scheduleName) throws Exception {
+ ScheduleId scheduleId = new ApplicationId(namespaceId, appName).schedule(scheduleName);
+ programScheduleService.delete(scheduleId);
+ responder.sendStatus(HttpResponseStatus.OK);
+ }
+
+ /**
+ * Returns the previous scheduled run time for all programs that are passed into the data. The
+ * data is an array of JSON objects where each object must contain the following three elements:
+ * appId, programType, and programId (flow name, service name, etc.).
+ *
+ * Example input:
+ *
+ * [{"appId": "App1", "programType": "Workflow", "programId": "WF1"},
+ * {"appId": "App1", "programType": "Workflow", "programId": "WF2"}]
+ *
+ *
+ * The response will be an array of JsonObjects each of which will contain the three input
+ * parameters as well as a "schedules" field, which is a list of {@link ScheduledRuntime} object.
+ *
+ * If an error occurs in the input (for the example above, App1 does not exist), then all
+ * JsonObjects for which the parameters have a valid status will have the status field but all
+ * JsonObjects for which the parameters do not have a valid status will have an error message and
+ * statusCode.
+ */
+ @POST
+ @Path("/previousruntime")
+ public void batchPreviousRunTimes(FullHttpRequest request,
+ HttpResponder responder,
+ @PathParam("namespace-id") String namespaceId) throws Exception {
+ List batchPrograms = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE);
+ responder.sendJson(HttpResponseStatus.OK,
+ ProgramHandlerUtil.toJson(batchRunTimes(namespaceId, batchPrograms, true)));
+ }
+
+ /**
+ * Returns the next scheduled run time for all programs that are passed into the data. The data is
+ * an array of JSON objects where each object must contain the following three elements: appId,
+ * programType, and programId (flow name, service name, etc.).
+ *
+ * Example input:
+ *
+ * [{"appId": "App1", "programType": "Workflow", "programId": "WF1"},
+ * {"appId": "App1", "programType": "Workflow", "programId": "WF2"}]
+ *
+ *
+ * The response will be an array of JsonObjects each of which will contain the three input
+ * parameters as well as a "schedules" field, which is a list of {@link ScheduledRuntime} object.
+ *
+ * If an error occurs in the input (for the example above, App1 does not exist), then all
+ * JsonObjects for which the parameters have a valid status will have the status field but all
+ * JsonObjects for which the parameters do not have a valid status will have an error message and
+ * statusCode.
+ */
+ @POST
+ @Path("/nextruntime")
+ public void batchNextRunTimes(FullHttpRequest request,
+ HttpResponder responder,
+ @PathParam("namespace-id") String namespaceId) throws Exception {
+ List batchPrograms = ProgramHandlerUtil.validateAndGetBatchInput(request, BATCH_PROGRAMS_TYPE);
+ responder.sendJson(HttpResponseStatus.OK,
+ ProgramHandlerUtil.toJson(batchRunTimes(namespaceId, batchPrograms, false)));
+ }
+
+ /**
+ * Fetches scheduled run times for a set of programs.
+ *
+ * @param namespace namespace of the programs
+ * @param programs the list of programs to fetch scheduled run times
+ * @param previous {@code true} to get the previous scheduled times; {@code false} to get the
+ * next scheduled times
+ * @return a list of {@link BatchProgramSchedule} containing the result
+ * @throws SchedulerException if failed to fetch schedules
+ */
+ private List batchRunTimes(String namespace,
+ Collection extends BatchProgram> programs,
+ boolean previous) throws Exception {
+ List programReferences = programs.stream()
+ .map(p -> new ProgramReference(namespace, p.getAppId(), p.getProgramType(),
+ p.getProgramId()))
+ .collect(Collectors.toList());
+ Map programMap = store.getPrograms(programReferences);
+
+ List result = new ArrayList<>();
+ for (ProgramReference programReference : programReferences) {
+ if (programMap.containsKey(programReference)) {
+ ProgramId programId = programMap.get(programReference);
+ result.add(new BatchProgramSchedule(programId, HttpResponseStatus.OK.code(), null,
+ getScheduledRunTimes(programId, previous)));
+ } else {
+ result.add(new BatchProgramSchedule(programReference, HttpResponseStatus.NOT_FOUND.code(),
+ new NotFoundException(programReference).getMessage(), null));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns a list of {@link ScheduledRuntime} for the given program.
+ *
+ * @param programId the program to fetch schedules for
+ * @param previous {@code true} to get the previous scheduled times; {@code false} to get the
+ * next scheduled times
+ * @return a list of {@link ScheduledRuntime}
+ * @throws SchedulerException if failed to fetch the schedule
+ */
+ private List getScheduledRunTimes(ProgramId programId,
+ boolean previous) throws Exception {
+ if (programId.getType().getSchedulableType() == null) {
+ throw new BadRequestException("Program " + programId + " cannot have schedule");
+ }
+
+ if (previous) {
+ return programScheduleService.getPreviousScheduledRuntimes(programId);
+ } else {
+ return programScheduleService.getNextScheduledRuntimes(programId);
+ }
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/meta/RemotePrivilegesHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/meta/RemotePrivilegesHandler.java
index 396cd45f12b5..cf4c5c737d75 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/meta/RemotePrivilegesHandler.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/meta/RemotePrivilegesHandler.java
@@ -34,6 +34,7 @@
import io.cdap.cdap.security.spi.authorization.AuditLogContext;
import io.cdap.cdap.security.spi.authorization.AuthorizationResponse;
import io.cdap.cdap.security.spi.authorization.PermissionManager;
+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.HttpResponseStatus;
@@ -81,25 +82,32 @@ public void enforce(FullHttpRequest request, HttpResponder responder) throws Exc
AuthorizationPrivilege authorizationPrivilege = GSON.fromJson(
request.content().toString(StandardCharsets.UTF_8),
AuthorizationPrivilege.class);
- LOG.debug("Enforcing for {}", authorizationPrivilege);
+ LOG.trace("Enforcing for {}", authorizationPrivilege);
Set permissions = authorizationPrivilege.getPermissions();
- if (authorizationPrivilege.getChildEntityType() != null) {
- //It's expected that we'll always have one, but let's handle generic case
- for (Permission permission : permissions) {
- accessEnforcer.enforceOnParent(authorizationPrivilege.getChildEntityType(),
- authorizationPrivilege.getEntity(),
- authorizationPrivilege.getPrincipal(), permission);
+ HttpResponseStatus responseStatus = HttpResponseStatus.OK;
+ try {
+ if (authorizationPrivilege.getChildEntityType() != null) {
+ //It's expected that we'll always have one, but let's handle generic case
+ for (Permission permission : permissions) {
+ accessEnforcer.enforceOnParent(authorizationPrivilege.getChildEntityType(),
+ authorizationPrivilege.getEntity(),
+ authorizationPrivilege.getPrincipal(), permission);
+ }
+ } else {
+ accessEnforcer.enforce(authorizationPrivilege.getEntity(),
+ authorizationPrivilege.getPrincipal(),
+ permissions);
}
- } else {
- accessEnforcer.enforce(authorizationPrivilege.getEntity(),
- authorizationPrivilege.getPrincipal(),
- permissions);
+ } catch (UnauthorizedException e) {
+ // In case of UnauthorizedException, we have the audit logs. So, propagating them with proper response instead of
+ // throwing it here as we will loose the audit logs. The caller should handle the logs and throw as needed.
+ responseStatus = HttpResponseStatus.valueOf(e.getStatusCode());
}
Queue auditLogContextQueue = SecurityRequestContext.getAuditLogQueue();
//Clearing this so it doesn't get double write to messaging queue
//This should be written by the Client who is calling this service.
SecurityRequestContext.clearAuditLogQueue();
- responder.sendJson(HttpResponseStatus.OK, GSON.toJson(auditLogContextQueue));
+ responder.sendJson(responseStatus, GSON.toJson(auditLogContextQueue));
}
@POST
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewErrorClassificationHttpHandler.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewErrorClassificationHttpHandler.java
index 6708e2b2a146..71f4bcb54ba6 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewErrorClassificationHttpHandler.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewErrorClassificationHttpHandler.java
@@ -20,37 +20,24 @@
import com.google.inject.Singleton;
import io.cdap.cdap.api.dataset.lib.CloseableIterator;
import io.cdap.cdap.app.preview.PreviewManager;
-import io.cdap.cdap.app.preview.PreviewStatus;
import io.cdap.cdap.app.preview.PreviewStatus.Status;
import io.cdap.cdap.common.NotFoundException;
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.common.logging.LoggingContext;
-import io.cdap.cdap.internal.app.store.RunRecordDetail;
import io.cdap.cdap.logging.ErrorLogsClassifier;
import io.cdap.cdap.logging.context.LoggingContextHelper;
import io.cdap.cdap.logging.filter.Filter;
import io.cdap.cdap.logging.filter.FilterParser;
-import io.cdap.cdap.logging.gateway.handlers.AbstractLogHttpHandler;
-import io.cdap.cdap.logging.gateway.handlers.ProgramRunRecordFetcher;
import io.cdap.cdap.logging.read.LogEvent;
import io.cdap.cdap.logging.read.LogOffset;
import io.cdap.cdap.logging.read.LogReader;
import io.cdap.cdap.logging.read.ReadRange;
-import io.cdap.cdap.proto.ProgramRunStatus;
-import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.id.ApplicationId;
-import io.cdap.cdap.proto.id.ProgramId;
-import io.cdap.cdap.proto.id.ProgramReference;
import io.cdap.cdap.proto.id.ProgramRunId;
-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.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
-import java.io.IOException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -88,6 +75,10 @@ private ProgramRunId getProgramRunId(ApplicationId applicationId) throws Excepti
return programRunId;
}
+ /**
+ * Returns the list of {@link io.cdap.cdap.proto.ErrorClassificationResponse} for
+ * failed preview run.
+ */
@POST
@Path("/namespaces/{namespace-id}/previews/{preview-id}/classify")
public void classifyRunIdLogs(HttpRequest request, HttpResponder responder,
@@ -110,7 +101,7 @@ public void classifyRunIdLogs(HttpRequest request, HttpResponder responder,
try (CloseableIterator logIter = logReader.getLog(loggingContext,
readRange.getFromMillis(), readRange.getToMillis(), filter)) {
// the iterator is closed by the BodyProducer passed to the HttpResponder
- errorLogsClassifier.classify(logIter, responder);
+ errorLogsClassifier.classify(logIter, responder, namespaceId, null, "preview", previewId);
} catch (Exception ex) {
LOG.debug("Exception while classifying logs for logging context {}", loggingContext, ex);
responder.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewHttpHandlerInternal.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewHttpHandlerInternal.java
index e35f453f2e7b..5fce15212830 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewHttpHandlerInternal.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/gateway/handlers/preview/PreviewHttpHandlerInternal.java
@@ -23,6 +23,7 @@
import io.cdap.cdap.app.preview.PreviewManager;
import io.cdap.cdap.app.preview.PreviewRequest;
import io.cdap.cdap.common.conf.Constants;
+import io.cdap.cdap.internal.app.runtime.k8s.PreviewRequestPollerInfo;
import io.cdap.http.AbstractHttpHandler;
import io.cdap.http.HttpHandler;
import io.cdap.http.HttpResponder;
@@ -57,7 +58,7 @@ public void poll(FullHttpRequest request, HttpResponder responder) {
if (previewRequest != null) {
LOG.debug("Send preview request {} to poller {}", previewRequest.getProgram(),
- Bytes.toString(pollerInfo));
+ GSON.fromJson(Bytes.toString(pollerInfo), PreviewRequestPollerInfo.class));
responder.sendString(HttpResponseStatus.OK, GSON.toJson(previewRequest));
} else {
responder.sendStatus(HttpResponseStatus.OK);
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/ApplicationSpecificationCodec.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/ApplicationSpecificationCodec.java
index 298fc8f3752d..7592a25fccd6 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/ApplicationSpecificationCodec.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/ApplicationSpecificationCodec.java
@@ -40,7 +40,7 @@
/**
* TODO: Move to cdap-proto
*/
-final class ApplicationSpecificationCodec extends
+public final class ApplicationSpecificationCodec extends
AbstractSpecificationCodec {
@Override
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/InMemoryProgramRunDispatcher.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/InMemoryProgramRunDispatcher.java
index 78199ddd9234..3bbe1bf42171 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/InMemoryProgramRunDispatcher.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/InMemoryProgramRunDispatcher.java
@@ -287,7 +287,7 @@ public ProgramController dispatchProgram(ProgramRunDispatcherContext dispatcherC
appSpec = generatedAppSpec != null ? generatedAppSpec : appSpec;
newProgramDescriptor = new ProgramDescriptor(programDescriptor.getProgramId(), appSpec);
} catch (Exception e) {
- LOG.warn("Failed to regenerate the app spec for program {}, using the existing app spec",
+ LOG.error("Failed to regenerate the app spec for program {}, using the existing app spec",
programId, e);
}
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/LocalApplicationManager.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/LocalApplicationManager.java
index ffc4ba0efaad..f270eb68ec17 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/LocalApplicationManager.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/LocalApplicationManager.java
@@ -40,10 +40,10 @@
import io.cdap.cdap.internal.app.deploy.pipeline.MetadataWriterStage;
import io.cdap.cdap.internal.app.deploy.pipeline.ProgramGenerationStage;
import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository;
+import io.cdap.cdap.internal.app.runtime.schedule.ScheduleManager;
import io.cdap.cdap.internal.capability.CapabilityReader;
import io.cdap.cdap.pipeline.Pipeline;
import io.cdap.cdap.pipeline.PipelineFactory;
-import io.cdap.cdap.scheduler.Scheduler;
import io.cdap.cdap.security.impersonation.Impersonator;
import io.cdap.cdap.security.impersonation.OwnerAdmin;
import io.cdap.cdap.security.spi.authentication.AuthenticationContext;
@@ -71,7 +71,7 @@ public class LocalApplicationManager implements Manager {
private final MetadataServiceClient metadataServiceClient;
private final Impersonator impersonator;
private final AuthenticationContext authenticationContext;
- private final io.cdap.cdap.scheduler.Scheduler programScheduler;
+ private final ScheduleManager scheduleManager;
private final AccessEnforcer accessEnforcer;
private final StructuredTableAdmin structuredTableAdmin;
private final CapabilityReader capabilityReader;
@@ -87,7 +87,7 @@ public class LocalApplicationManager implements Manager {
UsageRegistry usageRegistry, ArtifactRepository artifactRepository,
MetadataServiceClient metadataServiceClient,
Impersonator impersonator, AuthenticationContext authenticationContext,
- Scheduler programScheduler,
+ ScheduleManager scheduleManager,
AccessEnforcer accessEnforcer,
StructuredTableAdmin structuredTableAdmin,
CapabilityReader capabilityReader,
@@ -105,7 +105,7 @@ public class LocalApplicationManager implements Manager {
this.metadataServiceClient = metadataServiceClient;
this.impersonator = impersonator;
this.authenticationContext = authenticationContext;
- this.programScheduler = programScheduler;
+ this.scheduleManager = scheduleManager;
this.accessEnforcer = accessEnforcer;
this.structuredTableAdmin = structuredTableAdmin;
this.capabilityReader = capabilityReader;
@@ -128,11 +128,11 @@ public ListenableFuture deploy(I input) throws Exception {
pipeline.addLast(new CreateDatasetInstancesStage(cConf, datasetFramework, ownerAdmin,
authenticationContext));
pipeline.addLast(new DeletedProgramHandlerStage(store, programTerminator,
- metricsSystemClient, metadataServiceClient, programScheduler));
+ metricsSystemClient, metadataServiceClient, scheduleManager));
pipeline.addLast(new ProgramGenerationStage());
pipeline.addLast(new ApplicationRegistrationStage(store, usageRegistry, ownerAdmin,
metricsCollectionService));
- pipeline.addLast(new DeleteAndCreateSchedulesStage(programScheduler));
+ pipeline.addLast(new DeleteAndCreateSchedulesStage(scheduleManager));
pipeline.addLast(new MetadataWriterStage(metadataServiceClient));
pipeline.setFinally(new DeploymentCleanupStage());
return pipeline.execute(input);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeleteAndCreateSchedulesStage.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeleteAndCreateSchedulesStage.java
index 00b8d83ff7ab..bbdab7b86217 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeleteAndCreateSchedulesStage.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeleteAndCreateSchedulesStage.java
@@ -20,11 +20,11 @@
import io.cdap.cdap.api.app.ApplicationSpecification;
import io.cdap.cdap.api.schedule.Trigger;
import io.cdap.cdap.internal.app.runtime.schedule.ProgramSchedule;
+import io.cdap.cdap.internal.app.runtime.schedule.ScheduleManager;
import io.cdap.cdap.internal.schedule.ScheduleCreationSpec;
import io.cdap.cdap.pipeline.AbstractStage;
import io.cdap.cdap.proto.id.ApplicationId;
import io.cdap.cdap.proto.id.ProgramId;
-import io.cdap.cdap.scheduler.Scheduler;
import java.util.HashSet;
import java.util.Set;
@@ -33,11 +33,11 @@
*/
public class DeleteAndCreateSchedulesStage extends AbstractStage {
- private final Scheduler programScheduler;
+ private final ScheduleManager scheduleManager;
- public DeleteAndCreateSchedulesStage(Scheduler programScheduler) {
+ public DeleteAndCreateSchedulesStage(ScheduleManager scheduleManager) {
super(TypeToken.of(ApplicationWithPrograms.class));
- this.programScheduler = programScheduler;
+ this.scheduleManager = scheduleManager;
}
@Override
@@ -52,17 +52,17 @@ public void process(final ApplicationWithPrograms input) throws Exception {
ApplicationId appId = input.getApplicationId();
// Get a set of new schedules from the app spec
Set newSchedules = getProgramScheduleSet(appId, input.getSpecification());
- for (ProgramSchedule schedule : programScheduler.listSchedules(appId)) {
+ for (ProgramSchedule schedule : scheduleManager.listSchedules(appId)) {
if (newSchedules.contains(schedule)) {
newSchedules.remove(schedule); // Remove the existing schedule from the newSchedules
continue;
}
// Delete the existing schedule if it is not present in newSchedules
- programScheduler.deleteSchedule(schedule.getScheduleId());
+ scheduleManager.deleteSchedule(schedule.getScheduleId());
}
// Add new schedules
- programScheduler.addSchedules(newSchedules);
+ scheduleManager.addSchedules(newSchedules);
// Emit the input to next stage.
emit(input);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeletedProgramHandlerStage.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeletedProgramHandlerStage.java
index 0259d9965dec..fd9fc92e6bd4 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeletedProgramHandlerStage.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/deploy/pipeline/DeletedProgramHandlerStage.java
@@ -24,11 +24,11 @@
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.data2.metadata.writer.MetadataServiceClient;
import io.cdap.cdap.internal.app.deploy.ProgramTerminator;
+import io.cdap.cdap.internal.app.runtime.schedule.ScheduleManager;
import io.cdap.cdap.pipeline.AbstractStage;
import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.ProgramTypes;
import io.cdap.cdap.proto.id.ProgramId;
-import io.cdap.cdap.scheduler.Scheduler;
import io.cdap.cdap.spi.metadata.MetadataMutation;
import java.io.IOException;
import java.util.ArrayList;
@@ -53,18 +53,18 @@ public class DeletedProgramHandlerStage extends AbstractStage startPreview(PreviewRequest previewRequest) throws Exception {
+ public Future startPreview(PreviewRequest previewRequest)
+ throws Exception {
+ previewDataPublisher.setPublisherInfo(previewRequest.getRunnerInfo());
ProgramId programId = previewRequest.getProgram();
long submitTimeMillis = RunIds.getTime(programId.getApplication(), TimeUnit.MILLISECONDS);
previewStarted(programId);
@@ -208,7 +211,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/preview/DistributedPreviewManager.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewManager.java
index 08be41066108..b52c311e8a0f 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewManager.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewManager.java
@@ -23,7 +23,9 @@
import io.cdap.cdap.api.metrics.MetricsCollectionService;
import io.cdap.cdap.app.preview.PreviewConfigModule;
import io.cdap.cdap.app.preview.PreviewManager;
+import io.cdap.cdap.app.preview.PreviewRequest;
import io.cdap.cdap.app.preview.PreviewRequestQueue;
+import io.cdap.cdap.app.preview.PreviewStatus;
import io.cdap.cdap.app.store.preview.PreviewStore;
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
@@ -46,6 +48,8 @@
import io.cdap.cdap.master.spi.twill.StatefulDisk;
import io.cdap.cdap.master.spi.twill.StatefulTwillPreparer;
import io.cdap.cdap.messaging.spi.MessagingService;
+import io.cdap.cdap.proto.BasicThrowable;
+import io.cdap.cdap.proto.id.ApplicationId;
import io.cdap.cdap.proto.id.NamespaceId;
import io.cdap.cdap.security.authorization.AccessControllerInstantiator;
import io.cdap.cdap.security.spi.authentication.AuthenticationContext;
@@ -65,6 +69,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
import org.apache.hadoop.conf.Configuration;
import org.apache.tephra.TransactionSystemClient;
import org.apache.twill.api.ResourceSpecification;
@@ -88,6 +93,8 @@ public class DistributedPreviewManager extends DefaultPreviewManager implements
private final Configuration hConf;
private final FeatureFlagsProvider featureFlagsProvider;
private final TwillRunner twillRunner;
+ private final PreviewRunStopper previewRunStopper;
+ private final PreviewStore previewStore;
private ScheduledExecutorService scheduler;
private TwillController controller;
@@ -119,6 +126,8 @@ public class DistributedPreviewManager extends DefaultPreviewManager implements
this.hConf = hConf;
this.twillRunner = twillRunner;
this.featureFlagsProvider = new DefaultFeatureFlagsProvider(cConf);
+ this.previewRunStopper = previewRunStopper;
+ this.previewStore = previewStore;
}
@Override
@@ -308,6 +317,66 @@ public void run() {
controller = activeController;
}
+ /**
+ * Overrides the default poll behavior to enforce a "one-request-per-runner" policy.
+ *
+ * This implementation first checks if the polling runner is already associated with an active
+ * preview run. If it is, this indicates a protocol violation or a state inconsistency. In this
+ * case, the existing preview run is immediately marked as {@link PreviewStatus.Status#KILLED}
+ * with a reason indicating an invalid state. A best-effort attempt is then made to terminate the
+ * runner's container, and the poll request is denied by returning an empty {@link Optional}.
+ *
+ * If the runner is not associated with an existing run, this method delegates to the parent
+ * {@code poll} method to retrieve the next available preview request from the queue.
+ *
+ * @param pollerInfo Information that uniquely identifies the polling preview runner.
+ * @return {@link Optional} containing a {@link PreviewRequest} if a job is available for a valid
+ * runner, or {@link Optional#empty()} if the request is denied due to a protocol violation or if
+ * no job is currently available in the queue.
+ */
+ @Override
+ public Optional poll(@Nullable byte[] pollerInfo) {
+ // In a distributed environment, pollerInfo is always expected.
+ // Return empty to deny the request without throwing an exception.
+ if (pollerInfo == null) {
+ LOG.warn("poll() was called with a null pollerInfo in a distributed environment. "
+ + "This is unexpected. Denying the request.");
+ return Optional.empty();
+ }
+
+ // Check if the runner is already registered to an application.
+ // If no app is associated with this runner, it's a valid new runner.
+ // Proceed to the default polling behavior.
+ ApplicationId existingAppId = previewStore.getApplicationId(pollerInfo);
+ if (existingAppId == null) {
+ return super.poll(pollerInfo);
+ }
+
+ // The runner is already registered, which is a state violation.
+ // Handle the violation and deny the request.
+ handlePollingViolation(existingAppId, pollerInfo);
+ return Optional.empty();
+ }
+
+ private void handlePollingViolation(ApplicationId appId, byte[] pollerInfo) {
+ try {
+ PreviewStatus status = previewStore.getPreviewStatus(appId);
+ if (status != null && !status.getStatus().isEndState()) {
+ LOG.warn("Runner for application '{}' polled for a new request, indicating a state violation. "
+ + "Setting status to KILLED.", appId);
+ previewStore.setPreviewStatus(appId,
+ new PreviewStatus(PreviewStatus.Status.KILLED, status.getSubmitTime(),
+ new BasicThrowable(new IllegalStateException(
+ "Preview run stopped due to an invalid state. "
+ + "A runner can only be assigned one preview run at a time.")), null, null));
+ }
+ previewRunStopper.stop(pollerInfo);
+ } catch (Exception e) {
+ LOG.warn("Attempted to stop a runner due to a state violation but failed. "
+ + "The runner was still denied a new request.", e);
+ }
+ }
+
/**
* Deletes the given directory {@link Path} recursively.
*/
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewRunStopper.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewRunStopper.java
index 52ba1e472472..8b377adcbf22 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewRunStopper.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/DistributedPreviewRunStopper.java
@@ -56,7 +56,12 @@ public void stop(ApplicationId previewApp) throws Exception {
throw new IllegalStateException(
"Preview cannot be stopped. Please try stopping again or run the new preview.");
}
+ stop(info);
+ LOG.info("Force stopped preview run {}", previewApp);
+ }
+ @Override
+ public void stop(byte[] info) throws Exception {
PreviewRequestPollerInfo pollerInfo = GSON.fromJson(new String(info, StandardCharsets.UTF_8),
PreviewRequestPollerInfo.class);
Iterator controllers = twillRunner.lookup(PreviewRunnerTwillApplication.NAME)
@@ -65,20 +70,18 @@ public void stop(ApplicationId previewApp) throws Exception {
throw new IllegalStateException("Preview runners cannot be stopped. Please try again.");
}
- LOG.debug("Stopping preview run {} with poller info {}", previewApp, pollerInfo);
+ LOG.debug("Stopping preview run with poller info {}", pollerInfo);
TwillController controller = controllers.next();
Future future;
if (controller instanceof ExtendedTwillController) {
future = ((ExtendedTwillController) controller).restartInstance(
- PreviewRunnerTwillRunnable.class.getSimpleName(),
- pollerInfo.getInstanceId(),
+ PreviewRunnerTwillRunnable.class.getSimpleName(), pollerInfo.getInstanceId(),
pollerInfo.getInstanceUid());
} else {
future = controller.restartInstances(PreviewRunnerTwillRunnable.class.getSimpleName(),
pollerInfo.getInstanceId());
}
future.get();
- LOG.info("Force stopped preview run {}", previewApp);
}
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/MessagingPreviewDataPublisher.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/MessagingPreviewDataPublisher.java
index a4fa0401df50..f030d91bcbf5 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/MessagingPreviewDataPublisher.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/MessagingPreviewDataPublisher.java
@@ -28,9 +28,9 @@
import io.cdap.cdap.common.service.Retries;
import io.cdap.cdap.common.service.RetryStrategies;
import io.cdap.cdap.common.service.RetryStrategy;
+import io.cdap.cdap.messaging.client.StoreRequestBuilder;
import io.cdap.cdap.messaging.spi.MessagingService;
import io.cdap.cdap.messaging.spi.StoreRequest;
-import io.cdap.cdap.messaging.client.StoreRequestBuilder;
import io.cdap.cdap.proto.id.EntityId;
import io.cdap.cdap.proto.id.NamespaceId;
import io.cdap.cdap.proto.id.TopicId;
@@ -46,6 +46,7 @@ public class MessagingPreviewDataPublisher implements PreviewDataPublisher {
private final TopicId topic;
private final MessagingService messagingService;
private final RetryStrategy retryStrategy;
+ private byte[] publisherInfo;
@Inject
MessagingPreviewDataPublisher(CConfiguration cConf,
@@ -57,6 +58,11 @@ public class MessagingPreviewDataPublisher implements PreviewDataPublisher {
@Override
public void publish(EntityId entityId, PreviewMessage previewMessage) {
+ if (publisherInfo != null) {
+ // The publisherInfo is null in case of non-distributed environments, because there is no separate
+ // runner container. The preview runner is a thread in the same process.
+ previewMessage.setPublisherInfo(publisherInfo);
+ }
StoreRequest request = StoreRequestBuilder.of(topic).addPayload(GSON.toJson(previewMessage))
.build();
try {
@@ -68,4 +74,8 @@ public void publish(EntityId entityId, PreviewMessage previewMessage) {
e);
}
}
+
+ public void setPublisherInfo(byte[] publisherInfo) {
+ this.publisherInfo = publisherInfo;
+ }
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewDataSubscriberService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewDataSubscriberService.java
index 1cc348d26130..ad4e17df15ab 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewDataSubscriberService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewDataSubscriberService.java
@@ -35,9 +35,10 @@
import io.cdap.cdap.common.utils.ImmutablePair;
import io.cdap.cdap.internal.app.runtime.ProgramRunners;
import io.cdap.cdap.internal.app.store.AppMetadataStore;
-import io.cdap.cdap.messaging.spi.MessagingService;
import io.cdap.cdap.messaging.context.MultiThreadMessagingContext;
+import io.cdap.cdap.messaging.spi.MessagingService;
import io.cdap.cdap.messaging.subscriber.AbstractMessagingSubscriberService;
+import io.cdap.cdap.proto.BasicThrowable;
import io.cdap.cdap.proto.codec.EntityIdTypeAdapter;
import io.cdap.cdap.proto.id.ApplicationId;
import io.cdap.cdap.proto.id.EntityId;
@@ -45,6 +46,7 @@
import io.cdap.cdap.proto.id.ProgramRunId;
import io.cdap.cdap.spi.data.StructuredTableContext;
import io.cdap.cdap.spi.data.transaction.TransactionRunner;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@@ -68,6 +70,7 @@ public class PreviewDataSubscriberService extends
private final MultiThreadMessagingContext messagingContext;
private final TransactionRunner transactionRunner;
private final int maxRetriesOnError;
+ private final PreviewRunStopper previewRunStopper;
private int errorCount;
private String erroredMessageId;
private MetricsCollectionService metricsCollectionService;
@@ -81,7 +84,8 @@ public class PreviewDataSubscriberService extends
@Named(PreviewConfigModule.GLOBAL_METRICS) MetricsCollectionService
metricsCollectionService,
PreviewStore previewStore,
- TransactionRunner transactionRunner) {
+ TransactionRunner transactionRunner,
+ PreviewRunStopper previewRunStopper) {
super(
NamespaceId.SYSTEM.topic(cConf.get(Constants.Preview.MESSAGING_TOPIC)),
cConf.getInt(Constants.Metadata.MESSAGING_FETCH_SIZE),
@@ -101,6 +105,7 @@ public class PreviewDataSubscriberService extends
this.transactionRunner = transactionRunner;
this.maxRetriesOnError = cConf.getInt(Constants.Metadata.MESSAGING_RETRIES_ON_CONFLICT);
this.metricsCollectionService = metricsCollectionService;
+ this.previewRunStopper = previewRunStopper;
}
@Override
@@ -131,6 +136,9 @@ protected void processMessages(StructuredTableContext structuredTableContext,
ImmutablePair next = messages.next();
String messageId = next.getFirst();
PreviewMessage message = next.getSecond();
+ if (!validateMessage(message)) {
+ continue;
+ }
PreviewMessageProcessor processor = processors.computeIfAbsent(message.getType(), type -> {
switch (type) {
@@ -201,13 +209,6 @@ private final class PreviewDataProcessor implements PreviewMessageProcessor {
@Override
public void processMessage(PreviewMessage message) {
- if (!(message.getEntityId() instanceof ApplicationId)) {
- LOG.warn(
- "Missing application id from the preview data information. Ignoring the message {}",
- message);
- return;
- }
-
ApplicationId applicationId = (ApplicationId) message.getEntityId();
PreviewDataPayload payload;
try {
@@ -230,13 +231,6 @@ private final class PreviewStatusWriter implements PreviewMessageProcessor {
@Override
public void processMessage(PreviewMessage message) {
- if (!(message.getEntityId() instanceof ApplicationId)) {
- LOG.warn(
- "Missing application id from the preview status information. Ignoring the message {}",
- message);
- return;
- }
-
ApplicationId applicationId = (ApplicationId) message.getEntityId();
PreviewStatus payload;
try {
@@ -268,12 +262,6 @@ private final class PreviewProgramRunIdWriter implements PreviewMessageProcessor
@Override
public void processMessage(PreviewMessage message) {
- if (!(message.getEntityId() instanceof ApplicationId)) {
- LOG.warn("Missing application id from the preview run information. Ignoring the message {}",
- message);
- return;
- }
-
ProgramRunId payload;
try {
payload = message.getPayload(GSON, ProgramRunId.class);
@@ -286,4 +274,76 @@ public void processMessage(PreviewMessage message) {
previewStore.setProgramId(payload);
}
}
+
+ /**
+ * Validates an incoming {@link PreviewMessage} to enforce security policies. This method performs
+ * the following checks:
+ *
+ * - Ensures the message is associated with a valid {@link ApplicationId}.
+ * - Allows messages that do not contain publisher information to pass (for non-authenticated flows).
+ * - If publisher information is present, it validates that the message's {@code ApplicationId}
+ * matches the one registered for that publisher in the {@link PreviewStore}.
+ *
+ * If a mismatch is found, it is treated as a violation, and an attempt is made to
+ * terminate the offending runner.
+ *
+ * @param message the message to be validated.
+ * @return {@code true} if the message is valid, {@code false} otherwise.
+ */
+ private boolean validateMessage(PreviewMessage message) {
+ if (!(message.getEntityId() instanceof ApplicationId)) {
+ LOG.warn("Missing application id from the preview run information. Ignoring message: {}",
+ message);
+ return false;
+ }
+
+ byte[] messageRunnerInfo = message.getPublisherInfo();
+ // If there's no publisher info, the message is considered valid but unauthenticated.
+ // This allows for backward compatibility or simpler, non-secure, non-distributed environments.
+ if (messageRunnerInfo == null) {
+ return true;
+ }
+
+ ApplicationId appIdFromMessage = (ApplicationId) message.getEntityId();
+ byte[] registeredRunnerInfo = previewStore.getPreviewRequestPollerInfo(appIdFromMessage);
+
+ // Happy Path: If no runner is registered for this app yet, or if the messageRunnerInfo from the message
+ // matches the registered messageRunnerInfo, the message is valid.
+ if (Arrays.equals(registeredRunnerInfo, messageRunnerInfo)) {
+ return true;
+ }
+
+ // Failure Path: The publisher information does not match.
+ LOG.warn(
+ "Authentication failure: A message for application '{}' was received with a non-registered "
+ + "runner. Terminating the runner.", appIdFromMessage);
+ terminateRunner(messageRunnerInfo);
+
+ return false;
+ }
+
+ /**
+ * This method finds the application the suspicious runner was registered to, marks its status as
+ * KILLED, and then stops the runner's container.
+ */
+ private void terminateRunner(byte[] runnerInfo) {
+ try {
+ ApplicationId appId = previewStore.getApplicationId(runnerInfo);
+ if (appId != null) {
+ PreviewStatus status = previewStore.getPreviewStatus(appId);
+ if (status != null && !status.getStatus().isEndState()) {
+ previewStore.setPreviewStatus(appId,
+ new PreviewStatus(PreviewStatus.Status.KILLED,
+ status.getSubmitTime(), new BasicThrowable(new IllegalStateException(
+ "Preview run stopped due to an authentication failure."
+ + "Please try running preview again.")), null, null));
+ }
+ }
+ previewRunStopper.stop(runnerInfo);
+ } catch (Exception e) {
+ LOG.warn(
+ "Failed to stop runner after an authentication failure. The invalid message was still rejected.",
+ e);
+ }
+ }
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunStopper.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunStopper.java
index 01b2faea26fd..de090b92c208 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunStopper.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunStopper.java
@@ -30,4 +30,21 @@ public interface PreviewRunStopper {
* @throws Exception if any error while stopping
*/
void stop(ApplicationId preview) throws Exception;
+
+ /**
+ * Stops the preview runner associated with the given poller information.
+ *
+ * In a distributed environment (e.g., Kubernetes), this method is responsible for finding the
+ * runner's container and terminating it. This is a critical action for enforcing the
+ * "one-request-per-runner" security policy when a suspicious runner is detected.
+ *
+ * In a non-distributed (standalone) environment, this is a no-op as there is no separate container
+ * to terminate.
+ *
+ *
+ * @param pollerInfo The poller information identifying the preview runner to be stopped.
+ * @throws Exception if there is an error while attempting to stop the runner in a distributed
+ * environment.
+ */
+ void stop(byte[] pollerInfo) throws Exception;
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnable.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnable.java
index a8731b20c784..4fff6e20336e 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnable.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnable.java
@@ -67,7 +67,7 @@
import io.cdap.cdap.master.environment.MasterEnvironments;
import io.cdap.cdap.master.spi.environment.MasterEnvironment;
import io.cdap.cdap.master.spi.twill.ExtendedTwillContext;
-import io.cdap.cdap.messaging.guice.MessagingServiceModule;
+import io.cdap.cdap.messaging.guice.client.PreviewRunnerMessagingClientModule;
import io.cdap.cdap.proto.id.NamespaceId;
import io.cdap.cdap.security.auth.context.AuthenticationContextModules;
import io.cdap.cdap.security.authorization.AuthorizationEnforcementModule;
@@ -76,6 +76,7 @@
import io.cdap.cdap.security.impersonation.UGIProvider;
import io.cdap.cdap.spi.data.StorageProvider;
import java.io.File;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -168,12 +169,13 @@ private void doInitialize(TwillContext context) throws Exception {
hConf.clear();
hConf.addResource(new File(getArgument("hConf")).toURI().toURL());
+ byte[] secureToken = generateSecureToken();
PreviewRequestPollerInfo pollerInfo;
if (context instanceof ExtendedTwillContext) {
pollerInfo = new PreviewRequestPollerInfo(context.getInstanceId(),
- ((ExtendedTwillContext) context).getUID());
+ ((ExtendedTwillContext) context).getUID(), secureToken);
} else {
- pollerInfo = new PreviewRequestPollerInfo(context.getInstanceId(), null);
+ pollerInfo = new PreviewRequestPollerInfo(context.getInstanceId(), null, secureToken);
}
CConfiguration cConf = CConfiguration.create(new File(getArgument("cConf")).toURI().toURL());
@@ -237,7 +239,7 @@ protected void configure() {
}
modules.add(new PreviewRunnerManagerModule().getDistributedModules());
- modules.add(new MessagingServiceModule(cConf));
+ modules.add(new PreviewRunnerMessagingClientModule(cConf));
modules.add(new SecureStoreClientModule());
// Needed for InMemoryProgramRunnerModule. We use local metadata reader/publisher to avoid conflicting with
// metadata stored in AppFabric.
@@ -274,4 +276,14 @@ protected void configure() {
return Guice.createInjector(modules);
}
+
+ /**
+ * Generates a cryptographically strong random byte array to be used as a secret token.
+ */
+ private byte[] generateSecureToken() {
+ SecureRandom random = new SecureRandom();
+ byte[] token = new byte[32]; // 256 bits
+ random.nextBytes(token);
+ return token;
+ }
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/RemotePreviewRequestFetcher.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/RemotePreviewRequestFetcher.java
index d563de6bd78b..82023f1fc64a 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/RemotePreviewRequestFetcher.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/preview/RemotePreviewRequestFetcher.java
@@ -55,19 +55,24 @@ public class RemotePreviewRequestFetcher implements PreviewRequestFetcher {
@Override
public Optional fetch() throws IOException, UnauthorizedException {
+ byte[] runnerInfo = pollerInfoProvider.get();
HttpRequest request = remoteClientInternal.requestBuilder(HttpMethod.POST, "requests/pull")
- .withBody(ByteBuffer.wrap(pollerInfoProvider.get()))
+ .withBody(ByteBuffer.wrap(runnerInfo))
.build();
HttpResponse httpResponse = remoteClientInternal.execute(request);
if (httpResponse.getResponseCode() == 200) {
PreviewRequest previewRequest = GSON.fromJson(httpResponse.getResponseBodyAsString(),
PreviewRequest.class);
- if (previewRequest != null && previewRequest.getPrincipal() != null) {
- SecurityRequestContext.setUserId(previewRequest.getPrincipal().getName());
- SecurityRequestContext.setUserCredential(previewRequest.getPrincipal().getFullCredential());
- }
- return Optional.ofNullable(previewRequest);
+ return Optional.ofNullable(previewRequest)
+ .map(req -> {
+ if (req.getPrincipal() != null) {
+ SecurityRequestContext.setUserId(req.getPrincipal().getName());
+ SecurityRequestContext.setUserCredential(req.getPrincipal().getFullCredential());
+ }
+ req.setRunnerInfo(runnerInfo);
+ return req;
+ });
}
throw new IOException(
String.format("Received status code:%s and body: %s", httpResponse.getResponseCode(),
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisher.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisher.java
index 20067a04f654..c1cba7557f60 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisher.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisher.java
@@ -42,6 +42,7 @@
import io.cdap.cdap.proto.id.ProgramRunId;
import io.cdap.cdap.proto.id.TopicId;
import java.io.IOException;
+import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -142,9 +143,7 @@ public void publish(Notification.Type notificationType, Map prop
.build());
LOG.trace("Published program status notification: {}", programStatusNotification);
done = true;
- } catch (IOException | AccessException e) {
- throw Throwables.propagate(e);
- } catch (TopicNotFoundException | ServiceUnavailableException e) {
+ } catch (TopicNotFoundException | ServiceUnavailableException | SocketTimeoutException e) {
// These exceptions are retry-able due to TMS not completely started
if (startTime < 0) {
startTime = System.currentTimeMillis();
@@ -164,6 +163,8 @@ public void publish(Notification.Type notificationType, Map prop
Thread.currentThread().interrupt();
done = true;
}
+ } catch (AccessException | IOException e) {
+ throw Throwables.propagate(e);
}
}
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramControllerServiceAdapter.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramControllerServiceAdapter.java
index 24196f81e235..5a1c4b8159a0 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramControllerServiceAdapter.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/ProgramControllerServiceAdapter.java
@@ -16,8 +16,8 @@
package io.cdap.cdap.internal.app.runtime;
-import com.google.common.base.Throwables;
import com.google.common.util.concurrent.Service;
+import io.cdap.cdap.api.exception.WrappedStageException;
import io.cdap.cdap.app.runtime.ProgramController;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.common.logging.Loggers;
@@ -94,7 +94,7 @@ public void running() {
@Override
public void failed(Service.State from, Throwable failure) {
- Throwable rootCause = Throwables.getRootCause(failure);
+ Throwable rootCause = getRootCause(failure);
LOG.error("{} Program '{}' failed.", getProgramRunId().getType(),
getProgramRunId().getProgram(), failure);
USER_LOG.error(
@@ -104,6 +104,18 @@ public void failed(Service.State from, Throwable failure) {
error(failure);
}
+ private Throwable getRootCause(Throwable failure) {
+ Throwable cause;
+ while ((cause = failure.getCause()) != null) {
+ failure = cause;
+ if (WrappedStageException.class.isAssignableFrom(failure.getClass())) {
+ // to prevent stage information from getting lost from the log.
+ break;
+ }
+ }
+ return failure;
+ }
+
@Override
public void terminated(Service.State from) {
if (from != Service.State.STOPPING) {
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/artifact/Artifacts.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/Artifacts.java
index e23a7e788d81..7ddd4722a3f7 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/Artifacts.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/Artifacts.java
@@ -43,9 +43,12 @@ public static String getFileName(ArtifactId artifactId) {
* @return the resolved config type
* @throws IllegalArgumentException if the config type is not a valid type
*/
- public static Type getConfigType(Class extends Application> appClass) {
+ public static Type getConfigType(Class> appClass) {
+ // Class has to be a child of Application
+ Preconditions.checkArgument(Application.class.isAssignableFrom(appClass), "The given class : " + appClass
+ + " is not supported. Type must be a child class of Application");
TypeToken> configType = TypeToken.of(appClass)
- .resolveType(Application.class.getTypeParameters()[0]);
+ .resolveType(Application.class.getTypeParameters()[0]);
if (Reflections.isResolved(configType.getType())) {
// Default the type to Config.class if the resolved type is not subclass of Config.
// It normally won't happen, unless someone generate the bytecode directly.
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/DefaultArtifactInspector.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/DefaultArtifactInspector.java
index 354ea62030d7..662695ff324d 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/DefaultArtifactInspector.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/artifact/DefaultArtifactInspector.java
@@ -241,13 +241,19 @@ private ArtifactClasses.Builder inspectApplications(Id.Artifact artifactId,
return builder;
}
- Application app = (Application) mainClass.newInstance();
+ //If it's application assignable class, then ensure it has a public no-arg constructor
+ try {
+ mainClass.getConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new InvalidArtifactException(String.format(
+ "Application class %s in artifact %s must have a public no-arg constructor.", mainClassName, artifactId));
+ }
java.lang.reflect.Type configType;
// if the user parameterized their application, like 'xyz extends Application',
// we can deserialize the config into that object. Otherwise it'll just be a Config
try {
- configType = Artifacts.getConfigType(app.getClass());
+ configType = Artifacts.getConfigType(mainClass);
} catch (Exception e) {
throw new InvalidArtifactException(String.format(
"Could not resolve config type for Application class %s in artifact %s. "
@@ -258,7 +264,7 @@ private ArtifactClasses.Builder inspectApplications(Id.Artifact artifactId,
Schema configSchema =
configType == Config.class ? null : schemaGenerator.generate(configType);
builder.addApp(new ApplicationClass(mainClassName, "", configSchema,
- getArtifactRequirements(app.getClass())));
+ getArtifactRequirements(mainClass)));
} catch (ClassNotFoundException e) {
throw new InvalidArtifactException(String.format(
"Could not find Application main class %s in artifact %s.", mainClassName, artifactId));
@@ -267,10 +273,6 @@ private ArtifactClasses.Builder inspectApplications(Id.Artifact artifactId,
"Config for Application %s in artifact %s has an unsupported schema. "
+ "The type must extend Config and cannot be parameterized.", mainClassName,
artifactId));
- } catch (InstantiationException | IllegalAccessException e) {
- throw new InvalidArtifactException(String.format(
- "Could not instantiate Application class %s in artifact %s.", mainClassName, artifactId),
- e);
}
return builder;
@@ -459,8 +461,8 @@ private String getPluginDescription(Class> cls) {
}
/**
- * Returns the metadata mutation for this plugin, return {@code null} if no metadata annotation is
- * there
+ * Returns the metadata mutation for this plugin.
+ * return {@code null} if no metadata annotation is there.
*/
@Nullable
private MetadataMutation getMetadataMutation(PluginId pluginId, Class> cls)
@@ -622,7 +624,7 @@ private boolean isPlugin(String className, ClassLoader classLoader) {
// Use ASM to inspect the class bytecode to see if it is annotated with @Plugin
final boolean[] isPlugin = new boolean[1];
ClassReader cr = new ClassReader(is);
- cr.accept(new ClassVisitor(Opcodes.ASM5) {
+ cr.accept(new ClassVisitor(Opcodes.ASM7) {
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (Plugin.class.getName().equals(Type.getType(desc).getClassName()) && visible) {
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/distributed/runtimejob/DefaultRuntimeJob.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/distributed/runtimejob/DefaultRuntimeJob.java
index 9298e30b5f65..94d1d33eba49 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/distributed/runtimejob/DefaultRuntimeJob.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/distributed/runtimejob/DefaultRuntimeJob.java
@@ -243,7 +243,7 @@ public void run(RuntimeJobEnvironment runtimeJobEnv) throws Exception {
programDescriptor = new ProgramDescriptor(
programDescriptor.getProgramId(), appSpec);
} catch (Exception e) {
- LOG.warn("Failed to regenerate the app spec for program {}, using the existing app spec",
+ LOG.error("Failed to regenerate the app spec for program {}, using the existing app spec",
programId, e);
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/k8s/PreviewRequestPollerInfo.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/k8s/PreviewRequestPollerInfo.java
index 08db55df55ed..a0b38469b0f9 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/k8s/PreviewRequestPollerInfo.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/k8s/PreviewRequestPollerInfo.java
@@ -16,19 +16,32 @@
package io.cdap.cdap.internal.app.runtime.k8s;
+import com.google.common.base.Objects;
+import java.util.Arrays;
import javax.annotation.Nullable;
/**
- * Poller information holder.
+ * A holder for information that uniquely identifies and authenticates a preview runner. This object
+ * is serialized and stored in the PreviewStore.
*/
public class PreviewRequestPollerInfo {
private final int instanceId;
private final String instanceUid;
+ private final byte[] secureToken;
- public PreviewRequestPollerInfo(int instanceId, @Nullable String instanceUid) {
+ /**
+ * Constructs a new PreviewRequestPollerInfo.
+ *
+ * @param instanceId the instance id of the runner
+ * @param instanceUid the unique UID of the runner pod
+ * @param secureToken a secret token used to authenticate requests from the runner
+ */
+ public PreviewRequestPollerInfo(int instanceId, @Nullable String instanceUid,
+ @Nullable byte[] secureToken) {
this.instanceId = instanceId;
this.instanceUid = instanceUid;
+ this.secureToken = secureToken;
}
public int getInstanceId() {
@@ -42,9 +55,25 @@ public String getInstanceUid() {
@Override
public String toString() {
- return "PreviewRequestPollerInfo{"
- + "instanceId=" + instanceId
- + ", instanceUid='" + instanceUid + '\''
- + '}';
+ return "PreviewRequestPollerInfo{" + "instanceId=" + instanceId + ", instanceUid='"
+ + instanceUid + '\'' + '}';
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(instanceId, instanceUid, Arrays.hashCode(secureToken));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ PreviewRequestPollerInfo that = (PreviewRequestPollerInfo) o;
+ return instanceId == that.instanceId && Objects.equal(instanceUid, that.instanceUid)
+ && Arrays.equals(secureToken, that.secureToken);
}
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/LocalScheduleManager.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/LocalScheduleManager.java
new file mode 100644
index 000000000000..7ba652f8dff5
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/LocalScheduleManager.java
@@ -0,0 +1,163 @@
+/*
+ * 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.schedule;
+
+import com.google.common.base.Throwables;
+import com.google.inject.Inject;
+import io.cdap.cdap.common.AlreadyExistsException;
+import io.cdap.cdap.common.BadRequestException;
+import io.cdap.cdap.common.ConflictException;
+import io.cdap.cdap.common.NotFoundException;
+import io.cdap.cdap.common.ProfileConflictException;
+import io.cdap.cdap.common.conf.CConfiguration;
+import io.cdap.cdap.internal.app.runtime.SystemArguments;
+import io.cdap.cdap.internal.app.runtime.schedule.queue.JobQueueTable;
+import io.cdap.cdap.internal.app.runtime.schedule.store.ProgramScheduleStoreDataset;
+import io.cdap.cdap.internal.app.runtime.schedule.store.Schedulers;
+import io.cdap.cdap.internal.app.store.profile.ProfileStore;
+import io.cdap.cdap.messaging.spi.MessagingService;
+import io.cdap.cdap.proto.ProgramType;
+import io.cdap.cdap.proto.id.ProfileId;
+import io.cdap.cdap.proto.id.ScheduleId;
+import io.cdap.cdap.runtime.spi.profile.ProfileStatus;
+import io.cdap.cdap.spi.data.transaction.TransactionRunner;
+import io.cdap.cdap.spi.data.transaction.TransactionRunners;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code ScheduleManager} to manage program schedules. This class is meant to be used by in-memory
+ * modules and tests.
+ */
+public class LocalScheduleManager extends ScheduleManager {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LocalScheduleManager.class);
+ private final TimeSchedulerService timeSchedulerService;
+
+ /**
+ * Parameterized constructor for LocalScheduleManager.
+ *
+ * @param transactionRunner TransactionRunner
+ * @param messagingService MessagingService
+ * @param cConf CConfiguration
+ * @param timeSchedulerService TimeSchedulerService
+ */
+ @Inject
+ public LocalScheduleManager(TransactionRunner transactionRunner, MessagingService messagingService,
+ CConfiguration cConf, TimeSchedulerService timeSchedulerService) {
+ super(transactionRunner, messagingService, cConf);
+ this.timeSchedulerService = timeSchedulerService;
+ }
+
+ @Override
+ public void addSchedules(Iterable extends ProgramSchedule> schedules)
+ throws BadRequestException, NotFoundException, IOException, ConflictException {
+ for (ProgramSchedule schedule : schedules) {
+ if (!schedule.getProgramId().getType().equals(ProgramType.WORKFLOW)) {
+ throw new BadRequestException(String.format(
+ "Cannot schedule program %s of type %s: Only workflows can be scheduled",
+ schedule.getProgramId().getProgram(), schedule.getProgramId().getType()));
+ }
+ }
+
+ try {
+ TransactionRunners.run(transactionRunner, context -> {
+ ProgramScheduleStoreDataset store = Schedulers.getScheduleStore(context);
+ ProfileStore profileStore = ProfileStore.get(context);
+ long updatedTime = store.addSchedules(schedules);
+ for (ProgramSchedule schedule : schedules) {
+ if (schedule.getProperties() != null) {
+ Optional profile = SystemArguments.getProfileIdFromArgs(
+ schedule.getProgramId().getNamespaceId(), schedule.getProperties());
+ if (profile.isPresent()) {
+ ProfileId profileId = profile.get();
+ if (profileStore.getProfile(profileId).getStatus() == ProfileStatus.DISABLED) {
+ throw new ProfileConflictException(
+ String.format("Profile %s in namespace %s is disabled. It cannot "
+ + "be assigned to schedule %s",
+ profileId.getProfile(), profileId.getNamespace(),
+ schedule.getName()), profileId);
+ }
+ }
+ }
+ try {
+ timeSchedulerService.addProgramSchedule(schedule);
+ } catch (SchedulerException e) {
+ LOG.error("Exception occurs when adding schedule {}", schedule, e);
+ throw new RuntimeException(e);
+ }
+ }
+ for (ProgramSchedule schedule : schedules) {
+ ScheduleId scheduleId = schedule.getScheduleId();
+
+ // If the added properties contains profile assignment, add the assignment.
+ Optional profileId = SystemArguments.getProfileIdFromArgs(
+ scheduleId.getNamespaceId(),
+ schedule.getProperties());
+ if (profileId.isPresent()) {
+ profileStore.addProfileAssignment(profileId.get(), scheduleId);
+ }
+ }
+ // Publish the messages at the end of transaction.
+ for (ProgramSchedule schedule : schedules) {
+ adminEventPublisher.publishScheduleCreation(schedule.getScheduleId(), updatedTime);
+ }
+ return null;
+ }, Exception.class);
+ } catch (NotFoundException | ProfileConflictException | AlreadyExistsException e) {
+ throw e;
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void deleteSchedule(ScheduleId scheduleId)
+ throws NotFoundException, BadRequestException, IOException, ConflictException {
+ TransactionRunners.run(transactionRunner, context -> {
+ ProgramScheduleStoreDataset store = Schedulers.getScheduleStore(context);
+ ProfileStore profileStore = ProfileStore.get(context);
+ JobQueueTable queue = JobQueueTable.getJobQueue(context, cConf);
+ long deleteTime = System.currentTimeMillis();
+ List toNotify = new ArrayList<>();
+ ProgramSchedule schedule = store.getSchedule(scheduleId);
+ timeSchedulerService.deleteProgramSchedule(schedule);
+ queue.markJobsForDeletion(scheduleId, deleteTime);
+ toNotify.add(schedule);
+ // If the deleted schedule has properties with profile assignment, remove the assignment.
+ Optional profileId = SystemArguments.getProfileIdFromArgs(
+ scheduleId.getNamespaceId(),
+ schedule.getProperties());
+ if (profileId.isPresent()) {
+ try {
+ profileStore.removeProfileAssignment(profileId.get(), scheduleId);
+ } catch (NotFoundException e) {
+ // This should not happen since the profile cannot be deleted if there is a schedule who is using it.
+ LOG.warn("Unable to find the profile {} when deleting schedule {}, "
+ + "skipping assignment deletion.", profileId.get(), scheduleId);
+ }
+ }
+ store.deleteSchedule(scheduleId);
+ toNotify.forEach(adminEventPublisher::publishScheduleDeletion);
+ return null;
+ }, NotFoundException.class);
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/RemoteScheduleManager.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/RemoteScheduleManager.java
new file mode 100644
index 000000000000..1b421c6a6306
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/RemoteScheduleManager.java
@@ -0,0 +1,110 @@
+/*
+ * 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.schedule;
+
+import com.google.inject.Inject;
+import io.cdap.cdap.common.BadRequestException;
+import io.cdap.cdap.common.ConflictException;
+import io.cdap.cdap.common.NotFoundException;
+import io.cdap.cdap.common.conf.CConfiguration;
+import io.cdap.cdap.common.conf.Constants;
+import io.cdap.cdap.common.conf.Constants.Gateway;
+import io.cdap.cdap.common.http.DefaultHttpRequestConfig;
+import io.cdap.cdap.common.internal.remote.RemoteClient;
+import io.cdap.cdap.common.internal.remote.RemoteClientFactory;
+import io.cdap.cdap.gateway.handlers.util.ProgramHandlerUtil;
+import io.cdap.cdap.messaging.spi.MessagingService;
+import io.cdap.cdap.proto.ScheduleDetail;
+import io.cdap.cdap.proto.id.ScheduleId;
+import io.cdap.cdap.security.spi.authorization.UnauthorizedException;
+import io.cdap.cdap.spi.data.transaction.TransactionRunner;
+import io.cdap.common.http.HttpMethod;
+import io.cdap.common.http.HttpRequest;
+import io.cdap.common.http.HttpResponse;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+/**
+ * {@code ScheduleManager} to manage program schedules. This class uses remote client to communicate with
+ * {@code ProgramScheduleHttpHandler}.
+ */
+public class RemoteScheduleManager extends ScheduleManager {
+
+ private RemoteClient remoteClient;
+
+ /**
+ * Parameterized constructor for RemoteScheduleManager.
+ *
+ * @param transactionRunner TransactionRunner
+ * @param messagingService MessagingService
+ * @param cConf CConfiguration
+ * @param clientFactory RemoteClientFactory
+ */
+ @Inject
+ public RemoteScheduleManager(TransactionRunner transactionRunner, MessagingService messagingService,
+ CConfiguration cConf, RemoteClientFactory clientFactory) {
+ super(transactionRunner, messagingService, cConf);
+ this.remoteClient = clientFactory.createRemoteClient(Constants.Service.APP_FABRIC_PROCESSOR,
+ new DefaultHttpRequestConfig(false),
+ Gateway.API_VERSION_3);
+ }
+
+ @Override
+ public void addSchedules(Iterable extends ProgramSchedule> schedules)
+ throws BadRequestException, NotFoundException, IOException, ConflictException {
+ for (ProgramSchedule schedule : schedules) {
+ ScheduleId scheduleId = schedule.getScheduleId();
+ String url = String.format("namespaces/%s/apps/%s/schedules/%s",
+ scheduleId.getNamespace(),
+ scheduleId.getApplication(),
+ scheduleId.getSchedule());
+ HttpRequest.Builder requestBuilder = remoteClient.requestBuilder(HttpMethod.PUT, url);
+ ScheduleDetail scheduleDetail = schedule.toScheduleDetail();
+ requestBuilder.withBody(ProgramHandlerUtil.toJson(scheduleDetail, ScheduleDetail.class));
+ execute(requestBuilder.build());
+ }
+ }
+
+ @Override
+ public void deleteSchedule(ScheduleId scheduleId)
+ throws NotFoundException, BadRequestException, IOException, ConflictException {
+ String url = String.format("namespaces/%s/apps/%s/schedules/%s",
+ scheduleId.getNamespace(),
+ scheduleId.getApplication(),
+ scheduleId.getSchedule());
+ HttpRequest.Builder requestBuilder = remoteClient.requestBuilder(HttpMethod.DELETE, url);
+ execute(requestBuilder.build());
+ }
+
+ private HttpResponse execute(HttpRequest request)
+ throws IOException, NotFoundException, UnauthorizedException, BadRequestException, ConflictException {
+ HttpResponse httpResponse = remoteClient.execute(request);
+ switch (httpResponse.getResponseCode()) {
+ case HttpURLConnection.HTTP_OK:
+ return httpResponse;
+ case HttpURLConnection.HTTP_BAD_REQUEST:
+ throw new BadRequestException(httpResponse.getResponseBodyAsString());
+ case HttpURLConnection.HTTP_NOT_FOUND:
+ throw new NotFoundException(httpResponse.getResponseBodyAsString());
+ case HttpURLConnection.HTTP_CONFLICT:
+ throw new ConflictException(httpResponse.getResponseBodyAsString());
+ default:
+ throw new IOException(
+ String.format("Request failed %s", httpResponse.getResponseBodyAsString()));
+ }
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/ScheduleManager.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/ScheduleManager.java
new file mode 100644
index 000000000000..182f8f16dfd2
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/runtime/schedule/ScheduleManager.java
@@ -0,0 +1,144 @@
+/*
+ * 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.schedule;
+
+import io.cdap.cdap.common.BadRequestException;
+import io.cdap.cdap.common.ConflictException;
+import io.cdap.cdap.common.NotFoundException;
+import io.cdap.cdap.common.conf.CConfiguration;
+import io.cdap.cdap.internal.app.runtime.schedule.store.ProgramScheduleStoreDataset;
+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.profile.AdminEventPublisher;
+import io.cdap.cdap.messaging.context.MultiThreadMessagingContext;
+import io.cdap.cdap.messaging.spi.MessagingService;
+import io.cdap.cdap.proto.id.ApplicationId;
+import io.cdap.cdap.proto.id.ProgramId;
+import io.cdap.cdap.proto.id.ScheduleId;
+import io.cdap.cdap.spi.data.transaction.TransactionRunner;
+import io.cdap.cdap.spi.data.transaction.TransactionRunners;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Abstract class to manager program schedules.
+ */
+public abstract class ScheduleManager {
+
+ protected final CConfiguration cConf;
+ protected final TransactionRunner transactionRunner;
+ protected final AdminEventPublisher adminEventPublisher;
+
+ /**
+ * Parameterized constructor for ScheduleManager.
+ *
+ * @param transactionRunner TransactionRunner
+ * @param messagingService MessagingService
+ * @param cConf CConfiguration
+ */
+ public ScheduleManager(TransactionRunner transactionRunner,
+ MessagingService messagingService, CConfiguration cConf) {
+ this.cConf = cConf;
+ this.transactionRunner = transactionRunner;
+ MultiThreadMessagingContext messagingContext = new MultiThreadMessagingContext(messagingService);
+ this.adminEventPublisher = new AdminEventPublisher(cConf, messagingContext);
+ }
+
+ /**
+ * Add program schedules for the given collection of Program Schedules.
+ *
+ * @param schedules Collection of Program Schedules.
+ *
+ * @throws BadRequestException if the program type or not workflow.
+ * @throws NotFoundException if the program was not found.
+ * @throws IOException if there was an internal error in adding schedules.
+ * @throws ConflictException if the profile is disabled.
+ */
+ public abstract void addSchedules(Iterable extends ProgramSchedule> schedules)
+ throws Exception;
+
+ /**
+ * Deleted a program schedule for the given {@code scheduleId}.
+ *
+ * @param scheduleId for the schedule to be deleted.
+ *
+ * @throws NotFoundException if the schedule is not found.
+ * @throws IOException if an internal error occurred when deleting the schedule.
+ */
+ public abstract void deleteSchedule(ScheduleId scheduleId)
+ throws Exception;
+
+ /**
+ * Deletes all the schedules for the given {@code ApplicationId}.
+ *
+ * @param appId the {@code ApplicationId} whose schedules need to be deleted.
+ */
+ public void deleteSchedules(ApplicationId appId) {
+ TransactionRunners.run(transactionRunner, context -> {
+ ProgramScheduleStoreDataset store = Schedulers.getScheduleStore(context);
+ List programSchedules = store.listSchedules(appId);
+ for (ProgramSchedule programSchedule : programSchedules) {
+ deleteSchedule(programSchedule.getScheduleId());
+ }
+ }, RuntimeException.class);
+ }
+
+ /**
+ * Deletes all the schedules for the given {@code ProgramId}.
+ *
+ * @param programId the {@code ProgramId} whose schedules need to be deleted.
+ */
+ public void deleteSchedules(ProgramId programId) {
+ TransactionRunners.run(transactionRunner, context -> {
+ ProgramScheduleStoreDataset store = Schedulers.getScheduleStore(context);
+ List programSchedules = store.listSchedules(programId);
+ for (ProgramSchedule programSchedule : programSchedules) {
+ deleteSchedule(programSchedule.getScheduleId());
+ }
+ }, RuntimeException.class);
+ }
+
+ /**
+ * Update all schedules that can be triggered by the given deleted program. A schedule will be
+ * removed if the only {@link ProgramStatusTrigger} in it is triggered by the deleted program.
+ * Schedules with composite triggers will be updated if the composite trigger can still be
+ * satisfied after the program is deleted, otherwise the schedules will be deleted.
+ *
+ * @param programId the program id for which to delete the schedules.
+ */
+ public void modifySchedulesTriggeredByDeletedProgram(ProgramId programId) {
+ TransactionRunners.run(transactionRunner, context -> {
+ ProgramScheduleStoreDataset store = Schedulers.getScheduleStore(context);
+ List deletedSchedules = store.modifySchedulesTriggeredByDeletedProgram(programId);
+ deletedSchedules.forEach(adminEventPublisher::publishScheduleDeletion);
+ }, RuntimeException.class);
+ }
+
+ /**
+ * Lists all the schedules for the given {@code ApplicationId}.
+ *
+ * @param appId the {@code ApplicationId}.
+ *
+ * @return List of {@code ProgramSchedule}.
+ */
+ public List listSchedules(ApplicationId appId) {
+ return TransactionRunners.run(transactionRunner, context -> {
+ ProgramScheduleStoreDataset store = Schedulers.getScheduleStore(context);
+ return store.listSchedules(appId);
+ }, RuntimeException.class);
+ }
+}
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/AppFabricProcessorService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricProcessorService.java
index d2a2d891db66..97fafa95f819 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricProcessorService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricProcessorService.java
@@ -26,6 +26,7 @@
import io.cdap.cdap.app.runtime.ProgramRuntimeService;
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
+import io.cdap.cdap.common.conf.Constants.AppFabric;
import io.cdap.cdap.common.conf.Constants.Service;
import io.cdap.cdap.common.conf.SConfiguration;
import io.cdap.cdap.common.discovery.ResolvingDiscoverable;
@@ -42,7 +43,6 @@
import io.cdap.cdap.internal.sysapp.SystemAppManagementService;
import io.cdap.cdap.proto.id.NamespaceId;
import io.cdap.cdap.scheduler.CoreSchedulerService;
-import io.cdap.cdap.scheduler.ScheduleNotificationSubscriberService;
import io.cdap.cdap.security.auth.AuditLogSubscriberService;
import io.cdap.http.HttpHandler;
import io.cdap.http.NettyHttpService;
@@ -51,7 +51,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
-import javax.annotation.Nullable;
import org.apache.twill.common.Cancellable;
import org.apache.twill.discovery.DiscoveryService;
import org.slf4j.Logger;
@@ -75,13 +74,12 @@ public class AppFabricProcessorService extends AbstractIdleService {
private final RunRecordCorrectorService runRecordCorrectorService;
private final RunDataTimeToLiveService runDataTimeToLiveService;
private final ProgramRunStatusMonitorService programRunStatusMonitorService;
- private final RunRecordMonitorService runRecordCounterService;
+ private final FlowControlService runRecordCounterService;
private final CoreSchedulerService coreSchedulerService;
private final ProvisioningService provisioningService;
private final BootstrapService bootstrapService;
private final SystemAppManagementService systemAppManagementService;
private final OperationNotificationSubscriberService operationNotificationSubscriberService;
- private final ScheduleNotificationSubscriberService scheduleNotificationSubscriberService;
private final CConfiguration cConf;
private final SConfiguration sConf;
private final boolean sslEnabled;
@@ -111,10 +109,9 @@ public AppFabricProcessorService(CConfiguration cConf,
ProvisioningService provisioningService,
BootstrapService bootstrapService,
SystemAppManagementService systemAppManagementService,
- RunRecordMonitorService runRecordCounterService,
+ FlowControlService runRecordCounterService,
RunDataTimeToLiveService runDataTimeToLiveService,
- OperationNotificationSubscriberService operationNotificationSubscriberService,
- ScheduleNotificationSubscriberService scheduleNotificationSubscriberService) {
+ OperationNotificationSubscriberService operationNotificationSubscriberService) {
this.hostname = hostname;
this.discoveryService = discoveryService;
this.handlers = handlers;
@@ -137,7 +134,6 @@ public AppFabricProcessorService(CConfiguration cConf,
this.runRecordCounterService = runRecordCounterService;
this.runDataTimeToLiveService = runDataTimeToLiveService;
this.operationNotificationSubscriberService = operationNotificationSubscriberService;
- this.scheduleNotificationSubscriberService = scheduleNotificationSubscriberService;
}
/**
@@ -167,7 +163,6 @@ protected void startUp() throws Exception {
programStopSubscriberService.start(),
runRecordCorrectorService.start(),
programRunStatusMonitorService.start(),
- scheduleNotificationSubscriberService.start(),
coreSchedulerService.start(),
runRecordCounterService.start(),
runDataTimeToLiveService.start(),
@@ -188,7 +183,7 @@ protected void startUp() throws Exception {
Constants.AppFabric.DEFAULT_BOSS_THREADS))
.setWorkerThreadPoolSize(cConf.getInt(Constants.AppFabric.WORKER_THREADS,
Constants.AppFabric.DEFAULT_WORKER_THREADS))
- .setPort(cConf.getInt(Constants.AppFabric.SERVER_PORT));
+ .setPort(cConf.getInt(Constants.AppFabric.PROCESSOR_PORT));
if (sslEnabled) {
new HttpsEnabler().configureKeyStore(cConf, sConf).enable(httpServiceBuilder);
}
@@ -201,7 +196,6 @@ protected void startUp() throws Exception {
protected void shutDown() throws Exception {
LOG.info("Stopping AppFabric processor service.");
cancelHttpService.cancel();
- scheduleNotificationSubscriberService.stopAndWait();
coreSchedulerService.stopAndWait();
bootstrapService.stopAndWait();
systemAppManagementService.stopAndWait();
@@ -224,7 +218,7 @@ private Cancellable startHttpService(NettyHttpService httpService) throws Except
String announceAddress = cConf.get(Constants.Service.MASTER_SERVICES_ANNOUNCE_ADDRESS,
httpService.getBindAddress().getHostName());
- int announcePort = cConf.getInt(Constants.AppFabric.SERVER_ANNOUNCE_PORT,
+ int announcePort = cConf.getInt(AppFabric.PROCESSOR_ANNOUNCE_PORT,
httpService.getBindAddress().getPort());
final InetSocketAddress socketAddress = new InetSocketAddress(announceAddress, announcePort);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricServer.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricServer.java
index 3f6e6418a484..0d584668b8e4 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricServer.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/AppFabricServer.java
@@ -43,7 +43,6 @@
import io.cdap.cdap.internal.namespace.credential.NamespaceCredentialProviderService;
import io.cdap.cdap.internal.provision.ProvisioningService;
import io.cdap.cdap.proto.id.NamespaceId;
-import io.cdap.cdap.scheduler.CoreSchedulerService;
import io.cdap.cdap.sourcecontrol.RepositoryCleanupService;
import io.cdap.cdap.sourcecontrol.operationrunner.SourceControlOperationRunner;
import io.cdap.cdap.spi.data.transaction.TransactionRunner;
@@ -78,7 +77,6 @@ public class AppFabricServer extends AbstractIdleService {
private final ApplicationLifecycleService applicationLifecycleService;
private final Set servicesNames;
private final Set handlerHookNames;
- private final CoreSchedulerService coreSchedulerService;
private final CredentialProviderService credentialProviderService;
private final NamespaceCredentialProviderService namespaceCredentialProviderService;
private final ProvisioningService provisioningService;
@@ -107,7 +105,6 @@ public AppFabricServer(CConfiguration cConf, SConfiguration sConf,
ApplicationLifecycleService applicationLifecycleService,
@Named("appfabric.services.names") Set servicesNames,
@Named("appfabric.handler.hooks") Set handlerHookNames,
- CoreSchedulerService coreSchedulerService,
CredentialProviderService credentialProviderService,
NamespaceCredentialProviderService namespaceCredentialProviderService,
ProvisioningService provisioningService,
@@ -126,7 +123,6 @@ public AppFabricServer(CConfiguration cConf, SConfiguration sConf,
this.handlerHookNames = handlerHookNames;
this.applicationLifecycleService = applicationLifecycleService;
this.sslEnabled = cConf.getBoolean(Constants.Security.SSL.INTERNAL_ENABLED);
- this.coreSchedulerService = coreSchedulerService;
this.credentialProviderService = credentialProviderService;
this.namespaceCredentialProviderService = namespaceCredentialProviderService;
this.provisioningService = provisioningService;
@@ -155,7 +151,6 @@ protected void startUp() throws Exception {
provisioningService.start(),
applicationLifecycleService.start(),
bootstrapService.start(),
- coreSchedulerService.start(),
credentialProviderService.start(),
sourceControlOperationRunner.start(),
repositoryCleanupService.start()
@@ -209,7 +204,6 @@ protected void startUp() throws Exception {
@Override
protected void shutDown() throws Exception {
- coreSchedulerService.stopAndWait();
cancelHttpService.cancel();
applicationLifecycleService.stopAndWait();
bootstrapService.stopAndWait();
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleService.java
index f67673bf097b..22eef1f32806 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleService.java
@@ -79,6 +79,7 @@
import io.cdap.cdap.internal.app.runtime.artifact.ArtifactDetail;
import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository;
import io.cdap.cdap.internal.app.runtime.artifact.Artifacts;
+import io.cdap.cdap.internal.app.runtime.schedule.ScheduleManager;
import io.cdap.cdap.internal.app.store.ApplicationMeta;
import io.cdap.cdap.internal.app.store.RunRecordDetail;
import io.cdap.cdap.internal.app.store.state.AppStateKey;
@@ -112,7 +113,6 @@
import io.cdap.cdap.proto.security.Principal;
import io.cdap.cdap.proto.security.StandardPermission;
import io.cdap.cdap.proto.sourcecontrol.SourceControlMeta;
-import io.cdap.cdap.scheduler.Scheduler;
import io.cdap.cdap.security.impersonation.EntityImpersonator;
import io.cdap.cdap.security.impersonation.Impersonator;
import io.cdap.cdap.security.impersonation.OwnerAdmin;
@@ -165,7 +165,7 @@ public class ApplicationLifecycleService extends AbstractIdleService {
*/
private final CConfiguration cConf;
private final Store store;
- private final Scheduler scheduler;
+ private final ScheduleManager scheduleManager;
private final UsageRegistry usageRegistry;
private final PreferencesService preferencesService;
private final MetricsSystemClient metricsSystemClient;
@@ -189,7 +189,7 @@ public class ApplicationLifecycleService extends AbstractIdleService {
*/
@Inject
public ApplicationLifecycleService(CConfiguration cConf,
- Store store, Scheduler scheduler, UsageRegistry usageRegistry,
+ Store store, ScheduleManager scheduleManager, UsageRegistry usageRegistry,
PreferencesService preferencesService, MetricsSystemClient metricsSystemClient,
OwnerAdmin ownerAdmin, ArtifactRepository artifactRepository,
ManagerFactory managerFactory,
@@ -203,7 +203,7 @@ public ApplicationLifecycleService(CConfiguration cConf,
Constants.AppFabric.DEFAULT_APP_UPDATE_SCHEDULES);
this.batchSize = cConf.getInt(AppFabric.STREAMING_BATCH_SIZE);
this.store = store;
- this.scheduler = scheduler;
+ this.scheduleManager = scheduleManager;
this.usageRegistry = usageRegistry;
this.preferencesService = preferencesService;
this.metricsSystemClient = metricsSystemClient;
@@ -299,6 +299,22 @@ public boolean scanApplications(ScanApplicationsRequest request,
}
}
+ /**
+ * Get application count of the latest versions in the namespace.
+ *
+ * @param namespace namespace for which count is to be returned.
+ * @return Count of the applications in the namespace.
+ */
+ public long getApplicationsCount(NamespaceId namespace) {
+ if (namespace == null) {
+ throw new IllegalStateException("Application scan request without namespace");
+ }
+ // Get count should have the same permissions as list apps since its used in tandem.
+ accessEnforcer.enforceOnParent(EntityType.APPLICATION, namespace,
+ authenticationContext.getPrincipal(), StandardPermission.LIST);
+ return store.getApplicationCount(namespace);
+ }
+
private void processApplications(List> list,
Consumer consumer) {
@@ -777,7 +793,6 @@ private ApplicationId updateApplicationInternal(ApplicationId appId,
ApplicationUpdateResult> updateResult = app.updateConfig(updateContext);
updatedAppConfig = GSON.toJson(updateResult.getNewConfig(), configType);
}
- Principal requestingUser = authenticationContext.getPrincipal();
String versionId = appId.getVersion();
// If LCM flow is enabled - we generate specific versions of the app.
@@ -796,8 +811,8 @@ private ApplicationId updateApplicationInternal(ApplicationId appId,
.setConfigString(updatedAppConfig)
.setOwnerPrincipal(ownerPrincipal)
.setUpdateSchedules(false)
- .setChangeDetail(new ChangeDetail(null, appId.getVersion(), requestingUser == null ? null :
- requestingUser.getName(), System.currentTimeMillis()))
+ .setChangeDetail(new ChangeDetail(null, appId.getVersion(),
+ decodeUserId(authenticationContext), System.currentTimeMillis()))
.setDeployedApplicationSpec(appSpec)
.setIsUpgrade(true)
.build();
@@ -1102,7 +1117,7 @@ private ApplicationWithPrograms deployApp(NamespaceId namespaceId, @Nullable Str
ChangeDetail change = new ChangeDetail(
changeSummary == null ? null : changeSummary.getDescription(),
changeSummary == null ? null : changeSummary.getParentVersion(),
- requestingUser == null ? null : requestingUser.getName(),
+ decodeUserId(authenticationContext),
System.currentTimeMillis());
// deploy application with newly added artifact
AppDeploymentInfo deploymentInfo = AppDeploymentInfo.builder()
@@ -1339,9 +1354,9 @@ private void deletePreferences(ApplicationId appId, ApplicationSpecification app
*/
private void deleteApp(ApplicationId appId, ApplicationSpecification spec) throws IOException {
//Delete the schedules
- scheduler.deleteSchedules(appId);
+ scheduleManager.deleteSchedules(appId);
for (WorkflowSpecification workflowSpec : spec.getWorkflows().values()) {
- scheduler.modifySchedulesTriggeredByDeletedProgram(appId.workflow(workflowSpec.getName()));
+ scheduleManager.modifySchedulesTriggeredByDeletedProgram(appId.workflow(workflowSpec.getName()));
}
deleteMetrics(appId, spec);
@@ -1379,9 +1394,9 @@ private void deleteApp(ApplicationId appId, ApplicationSpecification spec) throw
*/
private void deleteAppVersion(ApplicationId appId, ApplicationSpecification spec) {
//Delete the schedules
- scheduler.deleteSchedules(appId);
+ scheduleManager.deleteSchedules(appId);
for (WorkflowSpecification workflowSpec : spec.getWorkflows().values()) {
- scheduler.modifySchedulesTriggeredByDeletedProgram(appId.workflow(workflowSpec.getName()));
+ scheduleManager.modifySchedulesTriggeredByDeletedProgram(appId.workflow(workflowSpec.getName()));
}
store.removeApplication(appId);
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/FlowControlService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/FlowControlService.java
new file mode 100644
index 000000000000..921cbe588e40
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/FlowControlService.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright © 2022 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.services;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.AbstractIdleService;
+import com.google.inject.Inject;
+import io.cdap.cdap.api.metrics.MetricsCollectionService;
+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.common.conf.CConfiguration;
+import io.cdap.cdap.common.conf.Constants;
+import io.cdap.cdap.internal.app.store.AppMetadataStore;
+import io.cdap.cdap.proto.ProgramRunStatus;
+import io.cdap.cdap.proto.id.NamespaceId;
+import io.cdap.cdap.proto.id.ProgramRunId;
+import io.cdap.cdap.spi.data.transaction.TransactionRunner;
+import io.cdap.cdap.spi.data.transaction.TransactionRunners;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Maintain and provides total number of launching and running run-records. This class is used by
+ * flow-control mechanism for launch requests.
+ */
+public class FlowControlService extends AbstractIdleService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FlowControlService.class);
+
+ private final MetricsCollectionService metricsCollectionService;
+ private final TransactionRunner transactionRunner;
+ private final int readStalenessSeconds;
+
+ private static final Map tags = ImmutableMap.of(
+ Constants.Metrics.Tag.NAMESPACE, NamespaceId.SYSTEM.getNamespace()
+ );
+
+ /**
+ * Monitors the program flow control.
+ *
+ * @param metricsCollectionService collect metrics
+ */
+ @Inject
+ public FlowControlService(
+ MetricsCollectionService metricsCollectionService,
+ TransactionRunner transactionRunner, CConfiguration cConf) {
+ this.metricsCollectionService = metricsCollectionService;
+ this.transactionRunner = transactionRunner;
+ this.readStalenessSeconds = cConf.getInt(Constants.Metrics.FlowControl.READ_STALENESS_SECONDS, 0);
+ }
+
+ @Override
+ protected void startUp() throws Exception {
+ LOG.info("FlowControlService started.");
+ }
+
+ @Override
+ protected void shutDown() throws Exception {
+ LOG.info("FlowControlService successfully shut down.");
+ }
+
+ /**
+ * Add a new in-flight launch request and return total number of launching and running programs.
+ *
+ * @param programRunId run id associated with the launch request
+ * @return total number of launching and running program runs.
+ */
+ public Counter addRequestAndGetCounter(ProgramRunId programRunId, ProgramOptions programOptions,
+ ProgramDescriptor programDescriptor) throws Exception {
+ if (RunIds.getTime(programRunId.getRun(), TimeUnit.MILLISECONDS) == -1) {
+ throw new Exception("None time-based UUIDs are not supported");
+ }
+
+ Counter counter = TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore store = AppMetadataStore.create(context);
+ store.recordProgramPending(programRunId,
+ programOptions.getArguments().asMap(),
+ programOptions.getUserArguments().asMap(),
+ programDescriptor.getArtifactId().toApiArtifactId());
+ int launchingCount = store.getFlowControlLaunchingCount(readStalenessSeconds);
+ int runningCount = store.getFlowControlRunningCount(readStalenessSeconds);
+ return new Counter(launchingCount, runningCount);
+ });
+ LOG.info("Added request with runId {}.", programRunId);
+ emitMetrics(Constants.Metrics.FlowControl.LAUNCHING_COUNT, counter.getLaunchingCount());
+
+ LOG.info(
+ "Counter has {} concurrent launching and {} running programs.",
+ counter.getLaunchingCount(),
+ counter.getRunningCount());
+ return counter;
+ }
+
+ /**
+ * Get total number of launching and running programs.
+ *
+ * @return Counter with total number of launching and running program runs.
+ */
+ public Counter getCounter() {
+ return TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore store = AppMetadataStore.create(context);
+ return new Counter(store.getFlowControlLaunchingCount(readStalenessSeconds),
+ store.getFlowControlRunningCount(readStalenessSeconds));
+ });
+ }
+
+ public void emitFlowControlMetrics() {
+ Counter counter = getCounter();
+ emitMetrics(Constants.Metrics.FlowControl.LAUNCHING_COUNT, counter.getLaunchingCount());
+ emitMetrics(Constants.Metrics.FlowControl.RUNNING_COUNT, counter.getRunningCount());
+ }
+
+ private void emitMetrics(String metricName, long value) {
+ LOG.trace("Setting metric {} to value {}", metricName, value);
+ metricsCollectionService.getContext(tags).gauge(metricName, value);
+ }
+
+ /**
+ * Counts the concurrent program runs.
+ */
+ public class Counter {
+
+ /**
+ * Total number of launch requests that have been accepted but still missing in metadata store +
+ * * total number of run records with {@link ProgramRunStatus#PENDING} status + total number of
+ * run records with {@link ProgramRunStatus#STARTING} status.
+ */
+ private final int launchingCount;
+
+ /**
+ * Total number of run records with {@link ProgramRunStatus#RUNNING} status + Total number of run
+ * records with {@link ProgramRunStatus#SUSPENDED} status + Total number of run records with
+ * {@link ProgramRunStatus#RESUMING} status.
+ */
+ private final int runningCount;
+
+ Counter(int launchingCount, int runningCount) {
+ this.launchingCount = launchingCount;
+ this.runningCount = runningCount;
+ }
+
+ public int getLaunchingCount() {
+ return launchingCount;
+ }
+
+ public int getRunningCount() {
+ return runningCount;
+ }
+ }
+}
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 8bd33edccd14..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,71 +129,52 @@ 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 RunRecordMonitorService runRecordMonitorService;
+
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,
ProvisionerNotifier provisionerNotifier, ProvisioningService provisioningService,
ProgramStateWriter programStateWriter, CapabilityReader capabilityReader,
ArtifactRepository artifactRepository,
- RunRecordMonitorService runRecordMonitorService) {
+ 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.runRecordMonitorService = runRecordMonitorService;
- }
-
- /**
- * 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);
+ this.flowControlService = flowControlService;
}
/**
@@ -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 extends EntityId> 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,10 +645,9 @@ public RunId runInternal(ProgramId programId, Map userArgs,
userId = userId == null ? "" : userId;
checkCapability(programDescriptor);
-
ProgramRunId programRunId = programId.run(runId);
- RunRecordMonitorService.Counter counter = runRecordMonitorService.addRequestAndGetCount(
- programRunId);
+ FlowControlService.Counter counter = flowControlService.addRequestAndGetCounter(
+ programRunId, programOptions, programDescriptor);
boolean done = false;
try {
@@ -765,7 +681,7 @@ public RunId runInternal(ProgramId programId, Map userArgs,
done = true;
} finally {
if (!done) {
- runRecordMonitorService.removeRequest(programRunId, false);
+ flowControlService.emitFlowControlMetrics();
}
}
@@ -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 88fe9dc5ff0e..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,11 +110,12 @@ 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;
private final Store store;
- private final RunRecordMonitorService runRecordMonitorService;
+ private final FlowControlService flowControlService;
private Service delegate;
private Set programCompletionNotifiers;
@@ -123,22 +126,24 @@ public class ProgramNotificationSubscriberService extends AbstractIdleService {
MetricsCollectionService metricsCollectionService,
ProvisionerNotifier provisionerNotifier,
ProgramLifecycleService programLifecycleService,
+ ProgramRuntimeService runtimeService,
ProvisioningService provisioningService,
ProgramStateWriter programStateWriter,
TransactionRunner transactionRunner,
Store store,
- RunRecordMonitorService runRecordMonitorService) {
+ FlowControlService flowControlService) {
this.messagingService = messagingService;
this.cConf = cConf;
this.metricsCollectionService = metricsCollectionService;
this.provisionerNotifier = provisionerNotifier;
this.programLifecycleService = programLifecycleService;
+ this.runtimeService = runtimeService;
this.provisioningService = provisioningService;
this.programStateWriter = programStateWriter;
this.transactionRunner = transactionRunner;
this.store = store;
- this.runRecordMonitorService = runRecordMonitorService;
+ this.flowControlService = flowControlService;
this.programCompletionNotifiers = Collections.emptySet();
}
@@ -163,8 +168,7 @@ protected void startUp() throws Exception {
}
private void emitFlowControlMetrics() {
- runRecordMonitorService.emitLaunchingMetrics();
- runRecordMonitorService.emitRunningMetrics();
+ flowControlService.emitFlowControlMetrics();
}
private void restoreActiveRuns() {
@@ -184,23 +188,22 @@ private void restoreActiveRuns() {
}
try {
LOG.info("Found active run: {}", runRecordDetail.getProgramRunId());
- if (runRecordDetail.getStatus() == ProgramRunStatus.PENDING) {
- runRecordMonitorService.addRequest(runRecordDetail.getProgramRunId());
- } else if (runRecordDetail.getStatus() == ProgramRunStatus.STARTING) {
- runRecordMonitorService.addRequest(runRecordDetail.getProgramRunId());
- // It is unknown what is the state of program runs in STARTING state.
- // A STARTING message is published again to retry STARTING logic.
+ if (runRecordDetail.getStatus() == ProgramRunStatus.STARTING) {
ProgramOptions programOptions =
new SimpleProgramOptions(
runRecordDetail.getProgramRunId().getParent(),
new BasicArguments(runRecordDetail.getSystemArgs()),
new BasicArguments(runRecordDetail.getUserArgs()));
+ ProgramDescriptor programDescriptor = this.store.loadProgram(
+ runRecordDetail.getProgramRunId().getParent());
+ // It is unknown what is the state of program runs in STARTING state.
+ // A STARTING message is published again to retry STARTING logic.
LOG.debug("Retrying to start run {}.", runRecordDetail.getProgramRunId());
programStateWriter.start(
runRecordDetail.getProgramRunId(),
programOptions,
null,
- this.store.loadProgram(runRecordDetail.getProgramRunId().getParent()));
+ programDescriptor);
}
} catch (Exception e) {
ProgramRunId programRunId = runRecordDetail.getProgramRunId();
@@ -231,10 +234,11 @@ private ProgramNotificationSingleTopicSubscriberService createChildService(
metricsCollectionService,
provisionerNotifier,
programLifecycleService,
+ runtimeService,
provisioningService,
programStateWriter,
transactionRunner,
- runRecordMonitorService,
+ flowControlService,
name,
topicName,
programCompletionNotifiers);
@@ -270,12 +274,13 @@ 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;
private final MetricsCollectionService metricsCollectionService;
private Set programCompletionNotifiers;
- private final RunRecordMonitorService runRecordMonitorService;
+ private final FlowControlService flowControlService;
private final boolean checkTxSeparation;
ProgramNotificationSingleTopicSubscriberService(
@@ -284,10 +289,11 @@ class ProgramNotificationSingleTopicSubscriberService
MetricsCollectionService metricsCollectionService,
ProvisionerNotifier provisionerNotifier,
ProgramLifecycleService programLifecycleService,
+ ProgramRuntimeService runtimeService,
ProvisioningService provisioningService,
ProgramStateWriter programStateWriter,
TransactionRunner transactionRunner,
- RunRecordMonitorService runRecordMonitorService,
+ FlowControlService flowControlService,
String name,
String topicName,
Set programCompletionNotifiers) {
@@ -305,12 +311,13 @@ 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<>();
this.metricsCollectionService = metricsCollectionService;
this.programCompletionNotifiers = programCompletionNotifiers;
- this.runRecordMonitorService = runRecordMonitorService;
+ this.flowControlService = flowControlService;
// If number of partitions equals 1, DB deadlock cannot happen as a result of concurrent
// modifications to
@@ -545,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);
@@ -582,7 +590,7 @@ private void handleProgramEvent(
appMetadataStore.recordProgramRunning(
programRunId, logicalStartTimeSecs, twillRunId, messageIdBytes);
writeToHeartBeatTable(recordedRunRecord, logicalStartTimeSecs, programHeartbeatTable);
- runRecordMonitorService.removeRequest(programRunId, true);
+ flowControlService.emitFlowControlMetrics();
long startDelayTime =
logicalStartTimeSecs - RunIds.getTime(programRunId.getRun(), TimeUnit.SECONDS);
emitStartingTimeMetric(programRunId, startDelayTime, recordedRunRecord);
@@ -660,7 +668,7 @@ private void handleProgramEvent(
Constants.Metrics.Program.PROGRAM_REJECTED_RUNS,
null)
.ifPresent(runnables::add);
- runRecordMonitorService.removeRequest(programRunId, true);
+ flowControlService.emitFlowControlMetrics();
break;
default:
// This should not happen
@@ -774,7 +782,7 @@ private RunRecordDetail handleProgramCompletion(
programCompletionNotifiers.forEach(
notifier ->
notifier.onProgramCompleted(programRunId, recordedRunRecord.getStatus()));
- runRecordMonitorService.removeRequest(programRunId, true);
+ flowControlService.emitFlowControlMetrics();
});
}
return recordedRunRecord;
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/RunRecordMonitorService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/RunRecordMonitorService.java
deleted file mode 100644
index 89c2fb0c8a9b..000000000000
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/services/RunRecordMonitorService.java
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- * Copyright © 2022 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.services;
-
-import com.google.common.util.concurrent.AbstractScheduledService;
-import com.google.inject.Inject;
-import io.cdap.cdap.api.metrics.MetricsCollectionService;
-import io.cdap.cdap.app.runtime.ProgramRuntimeService;
-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.conf.Constants.Metrics.FlowControl;
-import io.cdap.cdap.proto.ProgramRunStatus;
-import io.cdap.cdap.proto.ProgramType;
-import io.cdap.cdap.proto.id.ProgramRunId;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Executors;
-import java.util.concurrent.PriorityBlockingQueue;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import org.apache.twill.common.Threads;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Maintain and return total number of launching and running run-records. This class is used by
- * flow-control mechanism for launch requests. It also has a cleanup mechanism to automatically
- * remove old (i.e., configurable) entries from the counter as a safe-guard mechanism.
- */
-public class RunRecordMonitorService extends AbstractScheduledService {
-
- private static final Logger LOG = LoggerFactory.getLogger(RunRecordMonitorService.class);
-
- /**
- * Contains ProgramRunIds of runs that have been accepted, but have not been added to metadata
- * store plus all run records with {@link ProgramRunStatus#PENDING} or {@link
- * ProgramRunStatus#STARTING} status.
- */
- private final BlockingQueue launchingQueue;
-
- private final ProgramRuntimeService runtimeService;
- private final long ageThresholdSec;
- private final CConfiguration cConf;
- private final MetricsCollectionService metricsCollectionService;
- private final int maxConcurrentRuns;
- private ScheduledExecutorService executor;
-
- /**
- * Tracks the program runs.
- *
- * @param cConf configuration
- * @param runtimeService service to get info on programs
- * @param metricsCollectionService collect metrics
- */
- @Inject
- public RunRecordMonitorService(
- CConfiguration cConf,
- ProgramRuntimeService runtimeService,
- MetricsCollectionService metricsCollectionService) {
- this.cConf = cConf;
- this.runtimeService = runtimeService;
- this.metricsCollectionService = metricsCollectionService;
-
- this.launchingQueue =
- new PriorityBlockingQueue<>(
- 128, Comparator.comparingLong(o -> RunIds.getTime(o.getRun(), TimeUnit.MILLISECONDS)));
- this.ageThresholdSec = cConf.getLong(Constants.AppFabric.MONITOR_RECORD_AGE_THRESHOLD_SECONDS);
- this.maxConcurrentRuns = cConf.getInt(Constants.AppFabric.MAX_CONCURRENT_RUNS);
- }
-
- @Override
- protected void startUp() throws Exception {
- LOG.info("RunRecordMonitorService started.");
- }
-
- @Override
- protected void shutDown() throws Exception {
- if (executor != null) {
- executor.shutdownNow();
- }
- LOG.info("RunRecordMonitorService successfully shut down.");
- }
-
- @Override
- protected void runOneIteration() throws Exception {
- cleanupQueue();
- }
-
- @Override
- protected Scheduler scheduler() {
- return Scheduler.newFixedRateSchedule(
- 0, cConf.getInt(Constants.AppFabric.MONITOR_CLEANUP_INTERVAL_SECONDS), TimeUnit.SECONDS);
- }
-
- @Override
- protected final ScheduledExecutorService executor() {
- executor =
- Executors.newSingleThreadScheduledExecutor(
- Threads.createDaemonThreadFactory("run-record-monitor-service-cleanup-scheduler"));
- return executor;
- }
-
- /**
- * Add a new in-flight launch request and return total number of launching and running programs.
- *
- * @param programRunId run id associated with the launch request
- * @return total number of launching and running program runs.
- */
- public Counter addRequestAndGetCount(ProgramRunId programRunId) throws Exception {
- if (RunIds.getTime(programRunId.getRun(), TimeUnit.MILLISECONDS) == -1) {
- throw new Exception("None time-based UUIDs are not supported");
- }
-
- int launchingCount = addRequest(programRunId);
- int runningCount = getProgramsRunningCount();
-
- LOG.info(
- "Counter has {} concurrent launching and {} running programs.",
- launchingCount,
- runningCount);
- return new Counter(launchingCount, runningCount);
- }
-
- /**
- * Get imprecise (due to data races) total number of launching and running programs.
- *
- * @return total number of launching and running program runs.
- */
- public Counter getCount() {
- int launchingCount = launchingQueue.size();
- int runningCount = getProgramsRunningCount();
-
- return new Counter(launchingCount, runningCount);
- }
-
- /**
- * Add a new in-flight launch request.
- *
- * @param programRunId run id associated with the launch request
- */
- public int addRequest(ProgramRunId programRunId) {
- int result;
- synchronized (launchingQueue) {
- launchingQueue.add(programRunId);
- result = launchingQueue.size();
- }
- emitMetrics(Constants.Metrics.FlowControl.LAUNCHING_COUNT, result);
- LOG.info("Added request with runId {}.", programRunId);
- return result;
- }
-
- /**
- * Remove the request with the provided programRunId when the request is no longer launching.
- * I.e., not in-flight, not in {@link ProgramRunStatus#PENDING} and not in {@link
- * ProgramRunStatus#STARTING}
- *
- * @param programRunId of the request to be removed from launching queue.
- * @param emitRunningChange if true, also updates {@link
- * Constants.Metrics.FlowControl#RUNNING_COUNT}
- */
- public void removeRequest(ProgramRunId programRunId, boolean emitRunningChange) {
- if (launchingQueue.remove(programRunId)) {
- LOG.info(
- "Removed request with runId {}. Counter has {} concurrent launching requests.",
- programRunId,
- launchingQueue.size());
- emitMetrics(Constants.Metrics.FlowControl.LAUNCHING_COUNT, launchingQueue.size());
- }
-
- if (emitRunningChange) {
- emitRunningMetrics();
- }
- }
-
- public void emitLaunchingMetrics(long value) {
- emitMetrics(Constants.Metrics.FlowControl.LAUNCHING_COUNT, value);
- }
-
- /**
- * Emit the {@link Constants.Metrics.FlowControl#LAUNCHING_COUNT} metric for runs.
- */
- public void emitLaunchingMetrics() {
- emitMetrics(Constants.Metrics.FlowControl.LAUNCHING_COUNT, launchingQueue.size());
- }
-
-
- /**
- * Emit the {@link Constants.Metrics.FlowControl#RUNNING_COUNT} metric for runs.
- */
- public void emitRunningMetrics() {
- emitMetrics(FlowControl.RUNNING_COUNT, getProgramsRunningCount());
- }
-
- private void emitMetrics(String metricName, long value) {
- LOG.trace("Setting metric {} to value {}", metricName, value);
- metricsCollectionService.getContext(Collections.emptyMap()).gauge(metricName, value);
- }
-
- private void cleanupQueue() {
- while (true) {
- ProgramRunId programRunId = launchingQueue.peek();
- if (programRunId == null
- || RunIds.getTime(programRunId.getRun(), TimeUnit.MILLISECONDS) + (ageThresholdSec * 1000)
- >= System.currentTimeMillis()) {
- // Queue is empty or queue head has not expired yet.
- break;
- }
- // Queue head might have already been removed. So instead of calling poll, we call remove.
- if (launchingQueue.remove(programRunId)) {
- LOG.info("Removing request with runId {} due to expired retention time.", programRunId);
- }
- }
- // Always emit both metrics after cleanup.
- emitLaunchingMetrics();
- emitRunningMetrics();
- }
-
- /**
- * Returns the total number of programs in running state. The count includes batch (i.e., {@link
- * ProgramType#WORKFLOW}), streaming (i.e., {@link ProgramType#SPARK}) with no parent and
- * replication (i.e., {@link ProgramType#WORKER}) jobs.
- */
- private int getProgramsRunningCount() {
- List list =
- runtimeService.listAll(
- ProgramType.WORKFLOW, ProgramType.WORKER, ProgramType.SPARK, ProgramType.MAPREDUCE);
-
- int launchingCount = launchingQueue.size();
-
- // We use program controllers (instead of querying metadata store) to count the total number of
- // programs in running state.
- // A program controller is created when a launch request is in the middle of starting state.
- // Therefore, the returning running count is NOT precise.
- int impreciseRunningCount =
- (int) list.stream()
- .filter(r -> isRunning(r.getController().getState().getRunStatus()))
- .count();
-
- if (maxConcurrentRuns < 0 || (launchingCount + impreciseRunningCount < maxConcurrentRuns)) {
- // It is safe to return the imprecise value since either flow control for runs is disabled
- // (i.e., -1) or flow control will not reject an incoming request yet.
- return impreciseRunningCount;
- }
-
- // Flow control is at the threshold. We return the precise count.
- return (int) list.stream()
- .filter(
- r ->
- isRunning(r.getController().getState().getRunStatus())
- && !launchingQueue.contains(r.getController().getProgramRunId()))
- .count();
- }
-
- private boolean isRunning(ProgramRunStatus status) {
- if (status == ProgramRunStatus.RUNNING
- || status == ProgramRunStatus.SUSPENDED
- || status == ProgramRunStatus.RESUMING) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Counts the concurrent program runs.
- */
- public class Counter {
-
- /**
- * Total number of launch requests that have been accepted but still missing in metadata store +
- * * total number of run records with {@link ProgramRunStatus#PENDING} status + total number of
- * run records with {@link ProgramRunStatus#STARTING} status.
- */
- private final int launchingCount;
-
- /**
- * Total number of run records with {@link ProgramRunStatus#RUNNING} status + Total number of run
- * records with {@link ProgramRunStatus#SUSPENDED} status + Total number of run records with
- * {@link ProgramRunStatus#RESUMING} status.
- */
- private final int runningCount;
-
- Counter(int launchingCount, int runningCount) {
- this.launchingCount = launchingCount;
- this.runningCount = runningCount;
- }
-
- public int getLaunchingCount() {
- return launchingCount;
- }
-
- public int getRunningCount() {
- return runningCount;
- }
- }
-}
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/AppMetadataStore.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/AppMetadataStore.java
index c8d5e1def038..d895e5ccc1a4 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/AppMetadataStore.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/AppMetadataStore.java
@@ -20,6 +20,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
@@ -39,10 +40,12 @@
import io.cdap.cdap.common.ConflictException;
import io.cdap.cdap.common.app.RunIds;
import io.cdap.cdap.common.conf.Constants;
+import io.cdap.cdap.common.conf.Constants.AppMetaStore;
import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter;
import io.cdap.cdap.internal.app.runtime.ProgramOptionConstants;
import io.cdap.cdap.internal.app.runtime.SystemArguments;
import io.cdap.cdap.internal.app.runtime.workflow.BasicWorkflowToken;
+import io.cdap.cdap.internal.app.store.adapters.ApplicationMetaAdapter;
import io.cdap.cdap.proto.BasicThrowable;
import io.cdap.cdap.proto.ProgramRunCluster;
import io.cdap.cdap.proto.ProgramRunClusterStatus;
@@ -59,6 +62,7 @@
import io.cdap.cdap.proto.id.ProgramReference;
import io.cdap.cdap.proto.id.ProgramRunId;
import io.cdap.cdap.proto.sourcecontrol.SourceControlMeta;
+import io.cdap.cdap.spi.data.FieldSizeLimitExceededException;
import io.cdap.cdap.spi.data.SortOrder;
import io.cdap.cdap.spi.data.StructuredRow;
import io.cdap.cdap.spi.data.StructuredTable;
@@ -67,6 +71,7 @@
import io.cdap.cdap.spi.data.table.field.Field;
import io.cdap.cdap.spi.data.table.field.Fields;
import io.cdap.cdap.spi.data.table.field.Range;
+import io.cdap.cdap.spi.data.table.options.StaleReadOption;
import io.cdap.cdap.store.StoreDefinition;
import java.io.IOException;
import java.io.StringReader;
@@ -150,7 +155,18 @@ public class AppMetadataStore {
.put(ProgramRunStatus.REJECTED, TYPE_RUN_RECORD_COMPLETED)
.build();
+ private static final String TYPE_FLOW_CONTROL_LAUNCHING = "launching";
+ private static final String TYPE_FLOW_CONTROL_RUNNING = "running";
+ private static final String TYPE_FLOW_CONTROL_NONE = "";
+
+ // Program types controlled by flow-control mechanism.
+ private static final Set CONTROL_FLOW_PROGRAM_TYPES = ImmutableSet.of(ProgramType.MAPREDUCE,
+ ProgramType.WORKFLOW,
+ ProgramType.SPARK,
+ ProgramType.WORKER);
+
private final StructuredTableContext context;
+ private final boolean appSpecReductionEnabled;
private StructuredTable applicationSpecificationTable;
private StructuredTable applicationEditTable;
private StructuredTable workflowNodeStateTable;
@@ -158,6 +174,8 @@ public class AppMetadataStore {
private StructuredTable workflowsTable;
private StructuredTable programCountsTable;
private StructuredTable subscriberStateTable;
+ private StructuredTable pluginDataTable;
+ private StructuredTable universalPluginDataTable;
/**
* Static method for creating an instance of {@link AppMetadataStore}.
@@ -168,6 +186,8 @@ public static AppMetadataStore create(StructuredTableContext context) {
private AppMetadataStore(StructuredTableContext context) {
this.context = context;
+ this.appSpecReductionEnabled = AppMetaStore.APPSPEC_REDUCTION_SUPPORTED_STORAGE_PROVIDERS.contains(
+ context.getStorageProvider());
}
private StructuredTable getApplicationSpecificationTable() {
@@ -249,6 +269,29 @@ private StructuredTable getSubscriberStateTable() {
return subscriberStateTable;
}
+ @VisibleForTesting
+ StructuredTable getPluginDataTable() {
+ try {
+ if (pluginDataTable == null) {
+ pluginDataTable = context.getTable(StoreDefinition.ArtifactStore.PLUGIN_DATA_TABLE);
+ }
+ } catch (TableNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ return pluginDataTable;
+ }
+
+ private StructuredTable getUniversalPluginDataTable() {
+ try {
+ if (universalPluginDataTable == null) {
+ universalPluginDataTable = context.getTable(StoreDefinition.ArtifactStore.UNIV_PLUGIN_DATA_TABLE);
+ }
+ } catch (TableNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ return universalPluginDataTable;
+ }
+
/**
* Gets the {@link ApplicationMeta} of the given application.
*
@@ -348,7 +391,8 @@ public void scanApplications(ScanApplicationsRequest request,
boolean keepScanning = true;
while (iterator.hasNext() && keepScanning && limit > 0) {
StructuredRow row = iterator.next();
- AppScanEntry scanEntry = new AppScanEntry(row);
+ AppScanEntry scanEntry = new AppScanEntry(row, getPluginDataTable(),
+ getUniversalPluginDataTable(), appSpecReductionEnabled);
if (scanEntryPredicate.test(scanEntry)) {
keepScanning = func.apply(scanEntry);
limit--;
@@ -391,13 +435,29 @@ public long getApplicationCount() throws IOException {
Range.Bound.EXCLUSIVE),
Range.create(fields, Range.Bound.EXCLUSIVE, null,
Range.Bound.INCLUSIVE));
- // Count the latest version of app,
- // we treat latest=["true",null] as latest for backward compatibility.
+ return getApplicationSpecificationTable().count(ranges, createLatestFilterIndex());
+ }
+
+ /**
+ * Returns the number of applications in the given namespace.
+ *
+ * @param namespaceId namespace ID for which to count.
+ * @return the count of application in the namespace.
+ * @throws IOException if the count fails.
+ */
+ public long getNamespaceApplicationCount(NamespaceId namespaceId) throws IOException {
+ Collection ranges = ImmutableList.of(Range.singleton(
+ ImmutableList.of(Fields.stringField(StoreDefinition.AppMetadataStore.NAMESPACE_FIELD,
+ namespaceId.getNamespace()))));
+ return getApplicationSpecificationTable().count(ranges, createLatestFilterIndex());
+ }
+
+ private Collection> createLatestFilterIndex() {
+ // We treat latest=["true",null] as latest for backward compatibility.
// Prior to 6.8, all versions of an application were returned in the list apps api, not just the latest version.
- Collection> filterIndexes =
- ImmutableList.of(Fields.booleanField(StoreDefinition.AppMetadataStore.LATEST_FIELD, null),
- Fields.booleanField(StoreDefinition.AppMetadataStore.LATEST_FIELD, true));
- return getApplicationSpecificationTable().count(ranges, filterIndexes);
+ return ImmutableList.of(
+ Fields.booleanField(StoreDefinition.AppMetadataStore.LATEST_FIELD, null),
+ Fields.booleanField(StoreDefinition.AppMetadataStore.LATEST_FIELD, true));
}
@Nullable
@@ -697,9 +757,16 @@ public int createApplicationVersion(ApplicationId id, ApplicationMeta appMeta, b
}
// Add a new version of the app
- writeApplication(id.getNamespace(), id.getApplication(), id.getVersion(), appMeta.getSpec(),
- appMeta.getChange(),
- appMeta.getSourceControlMeta(), markAsLatest);
+ try {
+ writeApplication(id.getNamespace(), id.getApplication(), id.getVersion(), appMeta.getSpec(),
+ appMeta.getChange(),
+ appMeta.getSourceControlMeta(), markAsLatest);
+ } catch (FieldSizeLimitExceededException e) {
+ LOG.error("Application deployment failed: ", e);
+ throw new IllegalArgumentException(
+ "Application deployment failed as size exceeds the supported limit of " + e.getLimit()
+ + " characters");
+ }
return getApplicationEditNumber(
new ApplicationReference(id.getNamespaceId(), id.getApplication()));
}
@@ -715,10 +782,10 @@ void writeApplication(String namespaceId, String appId, String versionId,
void writeApplication(String namespaceId, String appId, String versionId,
ApplicationSpecification spec, @Nullable ChangeDetail change,
@Nullable SourceControlMeta sourceControlMeta, boolean markAsLatest) throws IOException {
+ ApplicationMeta meta = new ApplicationMeta(appId, spec, null, null);
+ String json = ApplicationMetaAdapter.toJson(meta, appSpecReductionEnabled);
writeApplicationSerialized(namespaceId, appId, versionId,
- GSON.toJson(
- new ApplicationMeta(appId, spec, null, null)),
- change, sourceControlMeta, markAsLatest);
+ json, change, sourceControlMeta, markAsLatest);
updateApplicationEdit(namespaceId, appId);
}
@@ -795,8 +862,9 @@ public void updateAppSpec(ApplicationId appId, ApplicationSpecification spec) th
}
// creation time cannot be null - will be written to app-spec but won't be added to table
ApplicationMeta updated = new ApplicationMeta(existing.getId(), spec, null);
+ String json = ApplicationMetaAdapter.toJson(updated, appSpecReductionEnabled);
updateApplicationSerialized(appId.getNamespace(), appId.getApplication(), appId.getVersion(),
- GSON.toJson(updated));
+ json);
}
/**
@@ -898,11 +966,58 @@ private void addWorkflowNodeState(ProgramRunId programRunId, Map
// Update the parent Workflow run record by adding node id and program run id in the properties
Map properties = new HashMap<>(record.getProperties());
properties.put(workflowNodeId, programRunId.getRun());
- writeToStructuredTableWithPrimaryKeys(
- runRecordFields,
- RunRecordDetail.builder(record).setProperties(properties).setSourceId(sourceId).build(),
- getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(runRecordFields,
+ RunRecordDetail.builder(record).setProperties(properties).setSourceId(sourceId).build());
+ }
+ }
+
+ /**
+ * Record that the program run is pending run.
+ *
+ * @param programRunId program run
+ *
+ * @return {@link RunRecordDetail} with status Pending.
+ */
+ @Nullable
+ public RunRecordDetail recordProgramPending(ProgramRunId programRunId, Map runtimeArgs,
+ Map systemArgs, @Nullable ArtifactId artifactId)
+ throws IOException {
+ long startTs = RunIds.getTime(programRunId.getRun(), TimeUnit.SECONDS);
+ if (startTs == -1L) {
+ throw new IllegalArgumentException(String.format(
+ "Provisioning state for program run that does not have a timestamp in the run id: '%s'", programRunId));
+ }
+
+ Optional profileId = SystemArguments.getProfileIdFromArgs(
+ programRunId.getNamespaceId(), systemArgs);
+ RunRecordDetail existing = getRun(programRunId);
+
+ // If for some reason, there is an existing run record then return null.
+ if (existing != null) {
+ LOG.warn(
+ "Ignoring unexpected request to record pending state for program run {} that has an "
+ + "existing run record in run state {} and cluster state {}.",
+ programRunId, existing.getStatus());
+ return null;
}
+
+ ProgramRunCluster cluster = new ProgramRunCluster(ProgramRunClusterStatus.PROVISIONING, null, null);
+ RunRecordDetail meta = RunRecordDetail.builder()
+ .setProgramRunId(programRunId)
+ .setStartTime(startTs)
+ .setProperties(getRecordProperties(systemArgs, runtimeArgs))
+ .setSystemArgs(systemArgs)
+ .setCluster(cluster)
+ .setStatus(ProgramRunStatus.PENDING)
+ .setPeerName(systemArgs.get(ProgramOptionConstants.PEER_NAME))
+ .setArtifactId(artifactId)
+ .setPrincipal(systemArgs.get(ProgramOptionConstants.PRINCIPAL))
+ .setProfileId(profileId.orElse(null))
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.PENDING, systemArgs))
+ .build();
+ writeNewRunRecord(meta, TYPE_RUN_RECORD_ACTIVE);
+ LOG.trace("Recorded {} for program {}", ProgramRunStatus.PENDING, programRunId);
+ return meta;
}
/**
@@ -927,25 +1042,6 @@ public RunRecordDetail recordProgramProvisioning(ProgramRunId programRunId,
Map systemArgs, byte[] sourceId,
@Nullable ArtifactId artifactId)
throws IOException {
- long startTs = RunIds.getTime(programRunId.getRun(), TimeUnit.SECONDS);
- if (startTs == -1L) {
- LOG.error(
- "Ignoring unexpected request to record provisioning state for program run {} that does not have "
-
- + "a timestamp in the run id.", programRunId);
- return null;
- }
-
- RunRecordDetail existing = getRun(programRunId);
- // for some reason, there is an existing run record.
- if (existing != null) {
- LOG.error(
- "Ignoring unexpected request to record provisioning state for program run {} that has an existing "
- + "run record in run state {} and cluster state {}.",
- programRunId, existing.getStatus(), existing.getCluster().getStatus());
- return null;
- }
-
Optional profileId = SystemArguments.getProfileIdFromArgs(
programRunId.getNamespaceId(), systemArgs);
if (!profileId.isPresent()) {
@@ -957,20 +1053,60 @@ public RunRecordDetail recordProgramProvisioning(ProgramRunId programRunId,
ProgramRunCluster cluster = new ProgramRunCluster(ProgramRunClusterStatus.PROVISIONING, null,
null);
- RunRecordDetail meta = RunRecordDetail.builder()
- .setProgramRunId(programRunId)
- .setStartTime(startTs)
- .setStatus(ProgramRunStatus.PENDING)
- .setProperties(getRecordProperties(systemArgs, runtimeArgs))
- .setSystemArgs(systemArgs)
- .setCluster(cluster)
- .setProfileId(profileId.get())
- .setPeerName(systemArgs.get(ProgramOptionConstants.PEER_NAME))
- .setSourceId(sourceId)
- .setArtifactId(artifactId)
- .setPrincipal(systemArgs.get(ProgramOptionConstants.PRINCIPAL))
- .build();
- writeNewRunRecord(meta, TYPE_RUN_RECORD_ACTIVE);
+
+ RunRecordDetail existing = getRun(programRunId);
+ RunRecordDetail meta;
+ if (existing == null) {
+ // Create a new run record if it doesn't exist.
+ long startTs = RunIds.getTime(programRunId.getRun(), TimeUnit.SECONDS);
+ if (startTs == -1L) {
+ LOG.error(
+ "Ignoring unexpected request to record provisioning state for program run {} that does not have "
+
+ + "a timestamp in the run id.", programRunId);
+ return null;
+ }
+ meta = RunRecordDetail.builder()
+ .setProgramRunId(programRunId)
+ .setStartTime(startTs)
+ .setStatus(ProgramRunStatus.PENDING)
+ .setProperties(getRecordProperties(systemArgs, runtimeArgs))
+ .setSystemArgs(systemArgs)
+ .setCluster(cluster)
+ .setProfileId(profileId.get())
+ .setPeerName(systemArgs.get(ProgramOptionConstants.PEER_NAME))
+ .setSourceId(sourceId)
+ .setArtifactId(artifactId)
+ .setPrincipal(systemArgs.get(ProgramOptionConstants.PRINCIPAL))
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.PENDING, systemArgs))
+ .build();
+ writeNewRunRecord(meta, TYPE_RUN_RECORD_ACTIVE);
+ } else {
+ if (existing.getStatus() != ProgramRunStatus.PENDING && existing.getStatus() != ProgramRunStatus.SUSPENDED) {
+ LOG.error(
+ "Ignoring unexpected request to record provisioning state for program run {} that has "
+ + "a status in end state {}.", programRunId, existing.getStatus());
+ return null;
+ }
+ delete(existing);
+ meta = RunRecordDetail.builder(existing)
+ .setStatus(ProgramRunStatus.PENDING)
+ .setProperties(getRecordProperties(systemArgs, runtimeArgs))
+ .setSystemArgs(systemArgs)
+ .setCluster(cluster)
+ .setProfileId(profileId.get())
+ .setPeerName(systemArgs.get(ProgramOptionConstants.PEER_NAME))
+ .setSourceId(sourceId)
+ .setArtifactId(artifactId)
+ .setPrincipal(systemArgs.get(ProgramOptionConstants.PRINCIPAL))
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.PENDING, systemArgs))
+ .build();
+ List> key = getProgramRunInvertedTimeKey(TYPE_RUN_RECORD_ACTIVE,
+ existing.getProgramRunId(),
+ existing.getStartTs());
+ writeRunRecordWithPrimaryKeys(key, meta);
+ }
+
LOG.trace("Recorded {} for program {}", ProgramRunClusterStatus.PROVISIONING, programRunId);
return meta;
}
@@ -1031,8 +1167,7 @@ public RunRecordDetail recordProgramProvisioned(ProgramRunId programRunId, int n
.setCluster(cluster)
.setSourceId(sourceId)
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunClusterStatus.PROVISIONED,
existing.getProgramRunId());
return meta;
@@ -1076,8 +1211,7 @@ public RunRecordDetail recordProgramDeprovisioning(ProgramRunId programRunId, by
.setCluster(cluster)
.setSourceId(sourceId)
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunClusterStatus.DEPROVISIONING,
existing.getProgramRunId());
return meta;
@@ -1122,8 +1256,7 @@ public RunRecordDetail recordProgramDeprovisioned(ProgramRunId programRunId, @Nu
.setCluster(cluster)
.setSourceId(sourceId)
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunClusterStatus.DEPROVISIONED,
existing.getProgramRunId());
return meta;
@@ -1167,10 +1300,9 @@ public RunRecordDetail recordProgramOrphaned(ProgramRunId programRunId, long end
.setCluster(cluster)
.setSourceId(sourceId)
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunClusterStatus.ORPHANED,
- existing.getProgramRunId());
+ meta.getProgramRunId());
return meta;
}
@@ -1179,32 +1311,30 @@ public RunRecordDetail recordProgramRejected(ProgramRunId programRunId,
Map runtimeArgs, Map systemArgs,
byte[] sourceId, @Nullable ArtifactId artifactId)
throws IOException {
- long startTs = RunIds.getTime(programRunId.getRun(), TimeUnit.SECONDS);
- if (startTs == -1L) {
- LOG.error(
- "Ignoring unexpected request to record provisioning state for program run {} that does not have "
-
- + "a timestamp in the run id.", programRunId);
- return null;
- }
-
RunRecordDetail existing = getRun(programRunId);
- // for some reason, there is an existing run record?
- if (existing != null) {
- LOG.error(
- "Ignoring unexpected request to record rejected state for program run {} that has an existing "
- + "run record in run state {} and cluster state {}.",
- programRunId, existing.getStatus(), existing.getCluster().getStatus());
- return null;
+
+ RunRecordDetail.Builder builder;
+ if (existing == null) {
+ LOG.warn(
+ "Unexpected request to record rejected state for program run {} that has no existing run record.",
+ programRunId);
+ long startTs = RunIds.getTime(programRunId.getRun(), TimeUnit.SECONDS);
+ if (startTs == -1L) {
+ LOG.error(
+ "Ignoring unexpected request to record provisioning state for program run {} that does not have "
+
+ + "a timestamp in the run id.", programRunId);
+ return null;
+ }
+ builder = RunRecordDetail.builder().setProgramRunId(programRunId).setStartTime(startTs).setStopTime(startTs);
+ } else {
+ delete(existing);
+ builder = RunRecordDetail.builder(existing).setStopTime(existing.getStartTs());
}
Optional profileId = SystemArguments.getProfileIdFromArgs(
programRunId.getNamespaceId(), systemArgs);
- RunRecordDetail meta = RunRecordDetail.builder()
- .setProgramRunId(programRunId)
- .setStartTime(startTs)
- .setStopTime(startTs) // rejected: stop time == start time
- .setStatus(ProgramRunStatus.REJECTED)
+ RunRecordDetail meta = builder.setStatus(ProgramRunStatus.REJECTED)
.setProperties(getRecordProperties(systemArgs, runtimeArgs))
.setSystemArgs(systemArgs)
.setProfileId(profileId.orElse(null))
@@ -1212,9 +1342,13 @@ public RunRecordDetail recordProgramRejected(ProgramRunId programRunId,
.setArtifactId(artifactId)
.setSourceId(sourceId)
.setPrincipal(systemArgs.get(ProgramOptionConstants.PRINCIPAL))
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.REJECTED, systemArgs))
.build();
- writeNewRunRecord(meta, TYPE_RUN_RECORD_COMPLETED);
+ List> key = getProgramRunInvertedTimeKey(TYPE_RUN_RECORD_COMPLETED,
+ meta.getProgramRunId(),
+ meta.getStartTs());
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunStatus.REJECTED, programRunId);
return meta;
}
@@ -1226,8 +1360,7 @@ private void writeNewRunRecord(RunRecordDetail meta, String typeRunRecordComplet
throws IOException {
List> fields = getProgramRunInvertedTimeKey(typeRunRecordCompleted,
meta.getProgramRunId(), meta.getStartTs());
- writeToStructuredTableWithPrimaryKeys(fields, meta, getRunRecordsTable(),
- StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(fields, meta);
List> countKey = getProgramCountPrimaryKeys(TYPE_COUNT,
meta.getProgramRunId().getParent());
getProgramCountsTable().increment(countKey, StoreDefinition.AppMetadataStore.COUNTS, 1L);
@@ -1280,9 +1413,9 @@ public RunRecordDetail recordProgramStart(ProgramRunId programRunId, @Nullable S
.setSystemArgs(newSystemArgs)
.setTwillRunId(twillRunId)
.setSourceId(sourceId)
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.STARTING, newSystemArgs))
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunStatus.STARTING, existing.getProgramRunId());
return meta;
}
@@ -1333,9 +1466,9 @@ public RunRecordDetail recordProgramRunning(ProgramRunId programRunId, long stat
.setStatus(ProgramRunStatus.RUNNING)
.setTwillRunId(twillRunId)
.setSourceId(sourceId)
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.RUNNING, systemArgs))
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunStatus.RUNNING, existing.getProgramRunId());
return meta;
}
@@ -1410,7 +1543,9 @@ private RunRecordDetail recordProgramSuspendResume(byte[] sourceId, RunRecordDet
List> key = getProgramRunInvertedTimeKey(TYPE_RUN_RECORD_ACTIVE,
existing.getProgramRunId(),
existing.getStartTs());
- RunRecordDetail.Builder builder = RunRecordDetail.builder(existing).setStatus(toStatus)
+ RunRecordDetail.Builder builder = RunRecordDetail.builder(existing)
+ .setStatus(toStatus)
+ .setFlowControlStatus(getFlowControlStatus(existing.getProgramRunId(), toStatus, existing.getSystemArgs()))
.setSourceId(sourceId);
if (timestamp != -1) {
if (action.equals("resume")) {
@@ -1420,8 +1555,7 @@ private RunRecordDetail recordProgramSuspendResume(byte[] sourceId, RunRecordDet
}
}
RunRecordDetail meta = builder.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", toStatus, existing.getProgramRunId());
return meta;
}
@@ -1475,9 +1609,9 @@ public RunRecordDetail recordProgramStopping(ProgramRunId programRunId, byte[] s
.setStoppingTime(stoppingTsSecs)
.setTerminateTs(terminateTsSecs)
.setSourceId(sourceId)
+ .setFlowControlStatus(getFlowControlStatus(programRunId, ProgramRunStatus.STOPPING, systemArgs))
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", ProgramRunStatus.STOPPING, existing.getProgramRunId());
return meta;
}
@@ -1529,9 +1663,9 @@ public RunRecordDetailWithExistingStatus recordProgramStop(ProgramRunId programR
.setStopTime(stopTs)
.setStatus(runStatus)
.setSourceId(sourceId)
+ .setFlowControlStatus(getFlowControlStatus(programRunId, runStatus, systemArgs))
.build();
- writeToStructuredTableWithPrimaryKeys(
- key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ writeRunRecordWithPrimaryKeys(key, meta);
LOG.trace("Recorded {} for program {}", runStatus, existing.getProgramRunId());
return meta;
}
@@ -1643,6 +1777,38 @@ public int countActiveRuns(@Nullable Integer limit) throws IOException {
return count.get();
}
+ /**
+ * Count all records in launching state.
+ *
+ * @return Count of records in launching state.
+ */
+ public int getFlowControlLaunchingCount(int readStalenessSeconds) throws IOException {
+ ImmutableList> keyPrefix = ImmutableList.of(
+ Fields.stringField(StoreDefinition.AppMetadataStore.RUN_STATUS, TYPE_RUN_RECORD_ACTIVE));
+ Collection> filterIndexes =
+ ImmutableList.of(
+ Fields.stringField(StoreDefinition.AppMetadataStore.FLOW_CONTROL_STATUS,
+ TYPE_FLOW_CONTROL_LAUNCHING));
+ return (int) getRunRecordsTable().count(Collections.singletonList(Range.singleton(keyPrefix)),
+ filterIndexes, new StaleReadOption(readStalenessSeconds));
+ }
+
+ /**
+ * Count all records in running state.
+ *
+ * @return Count of records in running state.
+ */
+ public int getFlowControlRunningCount(int readStalenessSeconds) throws IOException {
+ ImmutableList> keyPrefix = ImmutableList.of(
+ Fields.stringField(StoreDefinition.AppMetadataStore.RUN_STATUS, TYPE_RUN_RECORD_ACTIVE));
+ Collection> filterIndexes =
+ ImmutableList.of(
+ Fields.stringField(StoreDefinition.AppMetadataStore.FLOW_CONTROL_STATUS,
+ TYPE_FLOW_CONTROL_RUNNING));
+ return (int) getRunRecordsTable().count(Collections.singletonList(Range.singleton(keyPrefix)),
+ filterIndexes, new StaleReadOption(readStalenessSeconds));
+ }
+
/**
* Scans active runs, starting from the given cursor.
*
@@ -2519,6 +2685,7 @@ public void deleteAllAppMetadataTables() throws IOException {
deleteTable(getProgramCountsTable(), StoreDefinition.AppMetadataStore.COUNT_TYPE);
deleteTable(getSubscriberStateTable(), StoreDefinition.AppMetadataStore.SUBSCRIBER_TOPIC);
deleteTable(getApplicationEditTable(), StoreDefinition.AppMetadataStore.NAMESPACE_FIELD);
+ deleteTable(getPluginDataTable(), StoreDefinition.ArtifactStore.PARENT_NAMESPACE_FIELD);
}
private void deleteTable(StructuredTable table, String firstKey) throws IOException {
@@ -2703,9 +2870,12 @@ private ApplicationMeta decodeRow(StructuredRow row) {
String changeSummary = row.getString(StoreDefinition.AppMetadataStore.CHANGE_SUMMARY_FIELD);
Long creationTimeMillis = row.getLong(StoreDefinition.AppMetadataStore.CREATION_TIME_FIELD);
Boolean latest = row.getBoolean(StoreDefinition.AppMetadataStore.LATEST_FIELD);
- ApplicationMeta meta = GSON.fromJson(
+ String namespace = row.getString(StoreDefinition.AppMetadataStore.NAMESPACE_FIELD);
+
+ ApplicationMeta meta = ApplicationMetaAdapter.fromJson(
row.getString(StoreDefinition.AppMetadataStore.APPLICATION_DATA_FIELD),
- ApplicationMeta.class);
+ ApplicationMeta.class, namespace, appSpecReductionEnabled,
+ getPluginDataTable(), getUniversalPluginDataTable());
SourceControlMeta sourceControl = GSON.fromJson(
row.getString(StoreDefinition.AppMetadataStore.SOURCE_CONTROL_META),
SourceControlMeta.class);
@@ -2778,7 +2948,7 @@ private List> getRunRecordProgramPrefix(String status, @Nullable Progra
return getRunRecordProgramPrefix(status, programId.getProgramReference(),
programId.getVersion());
}
-
+
private List> getRunRecordProgramPrefix(String status,
@Nullable ProgramReference programRef, @Nullable String version) {
List> fields = getRunRecordStatusPrefix(status);
@@ -2920,6 +3090,33 @@ private static ApplicationId getApplicationIdFromRow(StructuredRow row) {
row.getString(StoreDefinition.AppMetadataStore.VERSION_FIELD));
}
+ private String getFlowControlStatus(ProgramRunId programRunId, ProgramRunStatus runStatus,
+ Map systemArgs) {
+ if (!CONTROL_FLOW_PROGRAM_TYPES.contains(programRunId.getType())) {
+ return TYPE_FLOW_CONTROL_NONE;
+ }
+ if (NamespaceId.SYSTEM.getNamespace().equals(programRunId.getParent().getNamespace())) {
+ return TYPE_FLOW_CONTROL_NONE;
+ }
+ if (systemArgs.containsKey(ProgramOptionConstants.WORKFLOW_NAME)) {
+ return TYPE_FLOW_CONTROL_NONE;
+ }
+ if (runStatus == ProgramRunStatus.RUNNING) {
+ return TYPE_FLOW_CONTROL_RUNNING;
+ }
+ if (runStatus == ProgramRunStatus.PENDING || runStatus == ProgramRunStatus.STARTING) {
+ return TYPE_FLOW_CONTROL_LAUNCHING;
+ }
+
+ return TYPE_FLOW_CONTROL_NONE;
+ }
+
+ private void writeRunRecordWithPrimaryKeys(List> key, RunRecordDetail meta) throws IOException {
+ key.add(Fields.stringField(StoreDefinition.AppMetadataStore.FLOW_CONTROL_STATUS, meta.getFlowControlStatus()));
+ writeToStructuredTableWithPrimaryKeys(
+ key, meta, getRunRecordsTable(), StoreDefinition.AppMetadataStore.RUN_RECORD_DATA);
+ }
+
/**
* Represents a position for scanning.
*/
@@ -2945,9 +3142,16 @@ private static final class AppScanEntry implements Map.Entry getAllAppVersionsAppIds(ApplicationReference appRef) {
return TransactionRunners.run(transactionRunner, context -> {
@@ -1179,6 +1191,14 @@ public void deleteAllStates(NamespaceId namespaceId, String appName)
}, ApplicationNotFoundException.class);
}
+ @Override
+ public long getApplicationCount(NamespaceId namespaceId) {
+ return TransactionRunners.run(transactionRunner, context -> {
+ return getAppMetadataStore(context).getNamespaceApplicationCount(namespaceId);
+ }
+ );
+ }
+
private AppStateTable getAppStateTable(StructuredTableContext context)
throws TableNotFoundException {
return new AppStateTable(context);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/AppSpecDeserializationContext.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/AppSpecDeserializationContext.java
new file mode 100644
index 000000000000..07972e077749
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/AppSpecDeserializationContext.java
@@ -0,0 +1,236 @@
+/*
+ * 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.store.adapters;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import io.cdap.cdap.api.artifact.ArtifactId;
+import io.cdap.cdap.api.plugin.PluginClass;
+import io.cdap.cdap.internal.app.runtime.plugin.PluginNotExistsException;
+import io.cdap.cdap.spi.data.StructuredRow;
+import io.cdap.cdap.spi.data.StructuredTable;
+import io.cdap.cdap.spi.data.table.field.Field;
+import io.cdap.cdap.spi.data.table.field.Fields;
+import io.cdap.cdap.store.StoreDefinition.ArtifactStore;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Nonnull;
+
+/**
+ * Provides context for deserializing an {@code ApplicationSpecification}, primarily for resolving
+ * and loading {@link PluginClass} instances. It holds the current namespace, caches loaded plugins,
+ * and allows an outer deserializer to set parent artifact details (name and namespace) used for
+ * plugin resolution.
+ */
+public class AppSpecDeserializationContext {
+
+ private static final Gson GSON = new Gson();
+ private ArtifactId rootArtifact;
+ private String namespace;
+ private String appName;
+ private final StructuredTable pluginDataTable;
+ private final StructuredTable universalPluginDataTable;
+ private Set missingPlugins;
+ private final LoadingCache pluginCache;
+
+ /**
+ * Constructs a context for deserializing an Application Specification.
+ *
+ * @param namespace The current application's namespace.
+ * @param pluginDataTable Table for loading plugin data.
+ * @param universalPluginDataTable Table for loading universal plugins.
+ */
+ public AppSpecDeserializationContext(String namespace, StructuredTable pluginDataTable,
+ StructuredTable universalPluginDataTable) {
+ this.namespace = namespace;
+ this.pluginDataTable = pluginDataTable;
+ this.universalPluginDataTable = universalPluginDataTable;
+ this.missingPlugins = new HashSet<>();
+ this.pluginCache = CacheBuilder.newBuilder().maximumSize(10)
+ .build(new CacheLoader() {
+ @Override
+ @Nonnull
+ public PluginClass load(@Nonnull PluginKey pluginKey) throws PluginNotExistsException {
+ return loadPlugin(pluginKey);
+ }
+ });
+ }
+
+ private PluginClass loadPlugin(PluginKey pluginKey) throws PluginNotExistsException {
+ PluginClass pluginClass = null;
+ Optional row = fetchFromPluginDataTable(pluginKey);
+ if (!row.isPresent()) {
+ row = fetchFromUniversalPluginDataTable(pluginKey);
+ if (!row.isPresent()) {
+ throw new PluginNotExistsException(
+ new io.cdap.cdap.proto.id.ArtifactId(pluginKey.getArtifactNamespace(),
+ pluginKey.getArtifactName(), pluginKey.getArtifactVersion()),
+ pluginKey.getPluginType(), pluginKey.getPluginName());
+ }
+ }
+ String rawPluginData = row.get().getString(ArtifactStore.PLUGIN_DATA_FIELD);
+ if (rawPluginData != null) {
+ JsonObject pluginDataObject = new JsonParser().parse(rawPluginData).getAsJsonObject();
+ if (pluginDataObject.has("pluginClass") && pluginDataObject.get("pluginClass")
+ .isJsonObject()) {
+ pluginClass = GSON.fromJson(pluginDataObject.getAsJsonObject("pluginClass"),
+ PluginClass.class);
+ }
+ }
+ if (pluginClass == null) {
+ throw new RuntimeException("Failed to deserialize PluginClass from data for key: " + pluginKey
+ + ". Raw data might be missing or malformed.");
+ }
+ return pluginClass;
+ }
+
+ private Optional fetchFromPluginDataTable(PluginKey pluginKey) {
+ if (pluginKey.getParentName() == null || pluginKey.getParentName().isEmpty()) {
+ // For such scenarios, we need to fetch plugin data from universal plugin data table.
+ return Optional.empty();
+ }
+ Collection> fields = new ArrayList<>();
+ fields.add(
+ Fields.stringField(ArtifactStore.PARENT_NAMESPACE_FIELD, pluginKey.getParentNamespace()));
+ fields.add(Fields.stringField(ArtifactStore.PARENT_NAME_FIELD, pluginKey.getParentName()));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_TYPE_FIELD, pluginKey.getPluginType()));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_NAME_FIELD, pluginKey.getPluginName()));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_NAMESPACE_FIELD,
+ pluginKey.getArtifactNamespace()));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_NAME_FIELD, pluginKey.getArtifactName()));
+ fields.add(
+ Fields.stringField(ArtifactStore.ARTIFACT_VER_FIELD, pluginKey.getArtifactVersion()));
+ try {
+ return pluginDataTable.read(fields);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read from plugin data table for key: " + pluginKey, e);
+ }
+ }
+
+ private Optional fetchFromUniversalPluginDataTable(PluginKey pluginKey) {
+ Collection> fields = new ArrayList<>();
+ fields.add(Fields.stringField(ArtifactStore.NAMESPACE_FIELD, namespace));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_TYPE_FIELD, pluginKey.getPluginType()));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_NAME_FIELD, pluginKey.getPluginName()));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_NAMESPACE_FIELD,
+ pluginKey.getArtifactNamespace()));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_NAME_FIELD, pluginKey.getArtifactName()));
+ fields.add(
+ Fields.stringField(ArtifactStore.ARTIFACT_VER_FIELD, pluginKey.getArtifactVersion()));
+ try {
+ return universalPluginDataTable.read(fields);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Failed to read from universal plugin data table for key: " + pluginKey, e);
+ }
+ }
+
+ /**
+ * Gets the primary namespace for the current deserialization.
+ *
+ * @return The namespace string.
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * Sets the primary namespace for the current deserialization.
+ */
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ /**
+ * Gets the root artifact, set externally.
+ */
+ public ArtifactId getRootArtifact() {
+ return rootArtifact;
+ }
+
+ /**
+ * Sets the root artifact for plugin resolution context.
+ */
+ public void setRootArtifact(ArtifactId rootArtifact) {
+ this.rootArtifact = rootArtifact;
+ }
+
+ /**
+ * Appends a plugin that could not be resolved during deserialization.
+ */
+ public void appendMissingPlugin(String pluginInfo) {
+ if (pluginInfo != null) {
+ this.missingPlugins.add(pluginInfo);
+ }
+ }
+
+ /**
+ * Returns a read-only view of the plugins that were missing during the deserialization process.
+ */
+ public Set getMissingPlugins() {
+ return Collections.unmodifiableSet(this.missingPlugins);
+ }
+
+ /**
+ * Sets the application name.
+ */
+ public void setAppName(String appName) {
+ this.appName = appName;
+ }
+
+ /**
+ * Gets the application name, set externally.
+ */
+ public String getAppName() {
+ return appName;
+ }
+
+ /**
+ * Retrieves the {@link PluginClass} for the given key, using an internal cache.
+ *
+ * @param pluginKey Key identifying the plugin.
+ * @return The resolved {@link PluginClass}.
+ * @throws RuntimeException if the plugin cannot be loaded.
+ */
+ public PluginClass getPlugin(PluginKey pluginKey) throws PluginNotExistsException {
+ try {
+ return this.pluginCache.get(pluginKey);
+ } catch (ExecutionException | UncheckedExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof PluginNotExistsException) {
+ // cache.get() method wraps the PluginNotExistsException with Execution exception.
+ // Hence, we need to explicitly re-throw the PluginNotExistsException here.
+ throw (PluginNotExistsException) cause;
+ }
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ }
+ throw new RuntimeException("Failed to load plugin class for " + pluginKey, cause);
+ }
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/AppSpecDeserializationContextHolder.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/AppSpecDeserializationContextHolder.java
new file mode 100644
index 000000000000..b017a35a72bf
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/AppSpecDeserializationContextHolder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.store.adapters;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Holds the {@link AppSpecDeserializationContext} for the current thread's deserialization
+ * operation. Ensures context is cleared after use.
+ */
+public final class AppSpecDeserializationContextHolder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(
+ AppSpecDeserializationContextHolder.class);
+ private static final ThreadLocal CURRENT_CONTEXT = new ThreadLocal<>();
+
+ private AppSpecDeserializationContextHolder() {
+ }
+
+ /**
+ * Sets the context for the current thread. Should be called before deserialization.
+ */
+ public static void setContext(AppSpecDeserializationContext context) {
+ if (CURRENT_CONTEXT.get() != null) {
+ LOG.warn("Overwriting existing AppSpecDeserializationContext on thread {}. "
+ + "This might indicate a missing clearContext() call from a previous operation.",
+ Thread.currentThread().getName());
+ }
+ CURRENT_CONTEXT.set(context);
+ }
+
+ /**
+ * Gets the context for the current thread.
+ *
+ * @throws IllegalStateException if no context is set.
+ */
+ public static AppSpecDeserializationContext getContext() {
+ AppSpecDeserializationContext context = CURRENT_CONTEXT.get();
+ if (context == null) {
+ throw new IllegalStateException(
+ "AppSpecDeserializationContext has not been set for the current deserialization operation on this thread.");
+ }
+ return context;
+ }
+
+ /**
+ * Clears the context for the current thread. Must be called in a finally block.
+ */
+ public static void clearContext() {
+ CURRENT_CONTEXT.remove();
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/ApplicationMetaAdapter.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/ApplicationMetaAdapter.java
new file mode 100644
index 000000000000..003211c2076d
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/ApplicationMetaAdapter.java
@@ -0,0 +1,111 @@
+/*
+ * 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.store.adapters;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import io.cdap.cdap.api.app.ApplicationSpecification;
+import io.cdap.cdap.api.plugin.Plugin;
+import io.cdap.cdap.api.plugin.PluginClass;
+import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter;
+import io.cdap.cdap.internal.app.ApplicationSpecificationCodec;
+import io.cdap.cdap.internal.app.store.ApplicationMeta;
+import io.cdap.cdap.spi.data.StructuredTable;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages Gson instances specifically configured for ApplicationMeta serialization and
+ * deserialization, handling application specification reduction. This ensures thread-safe
+ * operations by managing the {@link AppSpecDeserializationContext} lifecycle per operation via
+ * {@link AppSpecDeserializationContextHolder}. It uses a static cache for Gson instances based on
+ * the {@code appSpecReductionEnabled} flag.
+ */
+public final class ApplicationMetaAdapter {
+
+ private static final Gson GSON_INSTANCE_REDUCTION_ENABLED = buildGsonInternal(true);
+ private static final Gson GSON_INSTANCE_REDUCTION_DISABLED = buildGsonInternal(false);
+ private static final Logger LOG = LoggerFactory.getLogger(ApplicationMetaAdapter.class);
+
+ private ApplicationMetaAdapter() {
+ }
+
+ /**
+ * Deserializes a JSON string to an object of the specified class, managing the
+ * {@link AppSpecDeserializationContext} lifecycle via
+ * {@link AppSpecDeserializationContextHolder}.
+ */
+ public static T fromJson(String jsonString, Class classOfT, String namespace,
+ boolean appSpecReductionEnabled, StructuredTable pluginDataTable,
+ @Nullable StructuredTable universalPluginDataTable) {
+ AppSpecDeserializationContext operationContext = new AppSpecDeserializationContext(namespace,
+ pluginDataTable, universalPluginDataTable);
+ AppSpecDeserializationContextHolder.setContext(operationContext);
+ try {
+ Gson gson = getGson(appSpecReductionEnabled);
+ T result = gson.fromJson(jsonString, classOfT);
+ Set missingPlugins = operationContext.getMissingPlugins();
+ if (!missingPlugins.isEmpty()) {
+ LOG.trace("For application {} : {}", operationContext.getAppName(),
+ operationContext.getMissingPlugins());
+ }
+ return result;
+ } finally {
+ AppSpecDeserializationContextHolder.clearContext();
+ }
+ }
+
+ /**
+ * Serializes an object to its JSON representation.
+ */
+ public static String toJson(Object objectToSerialize, boolean appSpecReductionEnabled) {
+ Gson gson = getGson(appSpecReductionEnabled);
+ // No need to set / get the AppSpecDeserialization context in case of serialization.
+ return gson.toJson(objectToSerialize);
+ }
+
+ private static Gson buildGsonInternal(boolean appSpecReductionEnabled) {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+
+ if (appSpecReductionEnabled) {
+ gsonBuilder.registerTypeAdapter(ApplicationMeta.class, new ApplicationMetaCodec());
+ gsonBuilder.registerTypeAdapter(PluginClass.class, new PluginClassSerializer());
+ gsonBuilder.registerTypeAdapter(Plugin.class, new PluginDeserializer());
+ gsonBuilder.registerTypeHierarchyAdapter(ApplicationSpecification.class,
+ new ApplicationSpecificationCodec());
+ }
+ // This adds other necessary adapters for ApplicationSpecification internals (schemas, etc.)
+ ApplicationSpecificationAdapter.addTypeAdapters(gsonBuilder);
+
+ return gsonBuilder.create();
+ }
+
+ /**
+ * Gets a pre-configured, cached, and thread-safe Gson instance.
+ *
+ * @param conf Configuration map, used to determine if app spec reduction is enabled.
+ * @return A shared Gson instance.
+ */
+ private static Gson getGson(boolean appSpecReductionEnabled) {
+ if (appSpecReductionEnabled) {
+ return GSON_INSTANCE_REDUCTION_ENABLED;
+ }
+ return GSON_INSTANCE_REDUCTION_DISABLED;
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/ApplicationMetaCodec.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/ApplicationMetaCodec.java
new file mode 100644
index 000000000000..5332cc8ff911
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/ApplicationMetaCodec.java
@@ -0,0 +1,97 @@
+/*
+ * 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.store.adapters;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import io.cdap.cdap.api.app.ApplicationSpecification;
+import io.cdap.cdap.api.artifact.ArtifactId;
+import io.cdap.cdap.internal.app.store.ApplicationMeta;
+import java.lang.reflect.Type;
+
+/**
+ * Helper class to encode/decode {@link ApplicationMetaCodec} to/from json.
+ */
+public class ApplicationMetaCodec implements JsonSerializer,
+ JsonDeserializer {
+
+ /**
+ * Serializes an {@link ApplicationMeta} object to its JSON representation. Includes the
+ * application ID and its specification.
+ */
+ @Override
+ public JsonElement serialize(ApplicationMeta src, Type typeOfSrc,
+ JsonSerializationContext context) {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("id", src.getId());
+
+ JsonElement specJson = context.serialize(src.getSpec());
+ jsonObject.add("spec", specJson);
+
+ return jsonObject;
+ }
+
+ /**
+ * Deserializes a JSON element into an {@link ApplicationMeta} object. It extracts the
+ * application's {@link ArtifactId} from its specification JSON to set the parent name and
+ * namespace in the shared {@link AppSpecDeserializationContext}. This enables correct resolution
+ * of plugins within the application's scope.
+ *
+ * @throws JsonParseException If JSON is malformed or required fields are missing.
+ */
+ @Override
+ public ApplicationMeta deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+ AppSpecDeserializationContext appSpecDeserializationContext = AppSpecDeserializationContextHolder.getContext();
+ JsonObject jsonObject = json.getAsJsonObject();
+
+ JsonElement idElement = jsonObject.get("id");
+ if (idElement == null || idElement.isJsonNull()) {
+ throw new JsonParseException("ApplicationMeta 'id' field is missing or null");
+ }
+ String id = idElement.getAsString();
+
+ JsonObject specJson = jsonObject.getAsJsonObject("spec");
+ if (specJson == null || specJson.isJsonNull()) {
+ throw new JsonParseException("ApplicationMeta 'spec' field is missing or null for id: " + id);
+ }
+
+ // Set parent context before fully deserializing the AppSpec,
+ // so plugins can be retrieved from DB using this parent information.
+ JsonElement artifactIdJson = specJson.get("artifactId");
+ if (artifactIdJson != null && !artifactIdJson.isJsonNull()) {
+ ArtifactId artifactId = context.deserialize(artifactIdJson, ArtifactId.class);
+ if (artifactId != null) {
+ appSpecDeserializationContext.setRootArtifact(artifactId);
+ } else {
+ throw new JsonParseException(
+ "ArtifactId in ApplicationSpecification was null for app: " + id);
+ }
+ }
+
+ JsonElement appNameJson = specJson.get("name");
+ appSpecDeserializationContext.setAppName(appNameJson == null ? null : appNameJson.getAsString());
+
+ ApplicationSpecification spec = context.deserialize(specJson, ApplicationSpecification.class);
+ return new ApplicationMeta(id, spec, null, null);
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginClassSerializer.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginClassSerializer.java
new file mode 100644
index 000000000000..40769daff4b3
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginClassSerializer.java
@@ -0,0 +1,44 @@
+/*
+ * 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.store.adapters;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import io.cdap.cdap.api.plugin.PluginClass;
+import java.lang.reflect.Type;
+
+/**
+ * Gson serializer for {@link PluginClass}. Serializes only essential identification fields: 'type'
+ * and 'name'.
+ */
+public class PluginClassSerializer implements JsonSerializer {
+
+ /**
+ * Serializes a {@link PluginClass} to a JSON object. Only 'type' and 'name' fields are included
+ * in the serialized output, excluding other plugin information.
+ */
+ @Override
+ public JsonElement serialize(PluginClass src, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("type", src.getType());
+ jsonObject.addProperty("name", src.getName());
+ // Exclude other plugin information.
+ return jsonObject;
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginDeserializer.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginDeserializer.java
new file mode 100644
index 000000000000..f440297fc034
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginDeserializer.java
@@ -0,0 +1,144 @@
+/*
+ * 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.store.adapters;
+
+import com.google.common.collect.Iterables;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import io.cdap.cdap.api.artifact.ArtifactId;
+import io.cdap.cdap.api.artifact.ArtifactScope;
+import io.cdap.cdap.api.plugin.Plugin;
+import io.cdap.cdap.api.plugin.PluginClass;
+import io.cdap.cdap.api.plugin.PluginProperties;
+import io.cdap.cdap.internal.app.runtime.plugin.PluginNotExistsException;
+import io.cdap.cdap.internal.guava.reflect.TypeParameter;
+import io.cdap.cdap.internal.guava.reflect.TypeToken;
+import io.cdap.cdap.proto.id.NamespaceId;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Gson deserializer for {@link Plugin}s. Uses {@link AppSpecDeserializationContext} to enrich
+ * {@link PluginClass} data if initially minimal.
+ */
+public class PluginDeserializer implements JsonDeserializer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PluginDeserializer.class);
+
+ /**
+ * Deserializes JSON to {@link Plugin}. If {@link PluginClass#getClassName()} is empty, attempts
+ * to enrich it via {@link #resolvePluginClassData(List, ArtifactId, PluginClass)}.
+ *
+ * @param json The JSON element to deserialize.
+ * @param typeOfT The type of the object to deserialize to.
+ * @param context The Gson deserialization context.
+ * @return Deserialized {@link Plugin} object.
+ * @throws JsonParseException If JSON is malformed.
+ */
+ @Override
+ public Plugin deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ JsonObject jsonObject = json.getAsJsonObject();
+ List parents = deserializeList(jsonObject.get("parents"), context,
+ ArtifactId.class);
+ ArtifactId artifactId = context.deserialize(jsonObject.get("artifactId"), ArtifactId.class);
+ PluginClass pluginClass = context.deserialize(jsonObject.get("pluginClass"), PluginClass.class);
+
+ // Populate missing PluginClass data which was removed during serialization.
+ // This can be identified if the className field is empty.
+ if (pluginClass != null && (pluginClass.getClassName() == null || pluginClass.getClassName()
+ .isEmpty())) {
+ pluginClass = resolvePluginClassData(parents, artifactId, pluginClass);
+ }
+
+ PluginProperties properties = context.deserialize(jsonObject.get("properties"),
+ PluginProperties.class);
+
+ return new Plugin(parents, artifactId, pluginClass, properties);
+ }
+
+ /**
+ * Attempts to load {@link PluginClass} details from {@link AppSpecDeserializationContext}. Parent
+ * context for lookup defaults to application context; for some plugins, it may be overridden by
+ * artifact in the {@code parents} list if specific conditions are met. Returns the result of
+ * {@code appSpecDeserializationContext.getPlugin(pluginKey)}.
+ *
+ * @param parents Declared parent {@link ArtifactId}s from JSON.
+ * @param artifactId The plugin's own {@link ArtifactId}.
+ * @param pluginClass Initial {@link PluginClass}, possibly incomplete.
+ * @return {@link PluginClass} from context, or the outcome of the lookup.
+ */
+ private PluginClass resolvePluginClassData(List parents, ArtifactId artifactId,
+ PluginClass pluginClass) {
+ AppSpecDeserializationContext appSpecDeserializationContext = AppSpecDeserializationContextHolder.getContext();
+ String artifactNamespace = resolveNamespaceByScope(artifactId.getScope(),
+ appSpecDeserializationContext.getNamespace());
+
+ Exception exception = null;
+ ArtifactId rootArtifact = appSpecDeserializationContext.getRootArtifact();
+ for (ArtifactId parentId : Iterables.concat(parents, Collections.singleton(rootArtifact))) {
+ String parentName = parentId.getName();
+ String parentNamespace = resolveNamespaceByScope(parentId.getScope(),
+ appSpecDeserializationContext.getNamespace());
+ PluginKey pluginKey = new PluginKey(parentName, parentNamespace, artifactId.getName(),
+ artifactNamespace, artifactId.getVersion().getVersion(), pluginClass.getType(),
+ pluginClass.getName());
+ try {
+ return appSpecDeserializationContext.getPlugin(pluginKey);
+ } catch (PluginNotExistsException e) {
+ exception = e;
+ }
+ }
+ if (exception != null) {
+ appSpecDeserializationContext.appendMissingPlugin(exception.getMessage());
+ }
+ return pluginClass;
+ }
+
+ /**
+ * Deserializes a JSON array into a {@link List} of {@code valueType}. Returns an empty list if
+ * JSON is null, not an array, or if deserialization results in null.
+ */
+ private List deserializeList(JsonElement json, JsonDeserializationContext context,
+ Class valueType) {
+ if (json == null || json.isJsonNull() || !json.isJsonArray()) {
+ return Collections.emptyList();
+ }
+ Type type = new TypeToken>() {
+ }.where(new TypeParameter() {
+ }, valueType).getType();
+ List list = context.deserialize(json, type);
+ return list == null ? Collections.emptyList() : list;
+ }
+
+ /**
+ * Resolves namespace string from {@link ArtifactScope}. Uses system namespace for
+ * {@link ArtifactScope#SYSTEM}, otherwise from context.
+ *
+ * @param scope The artifact scope.
+ * @return Corresponding namespace string.
+ */
+ private String resolveNamespaceByScope(ArtifactScope scope, String namespace) {
+ return ArtifactScope.SYSTEM.equals(scope) ? NamespaceId.SYSTEM.getNamespace() : namespace;
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginKey.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginKey.java
new file mode 100644
index 000000000000..70f53476f686
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/adapters/PluginKey.java
@@ -0,0 +1,142 @@
+/*
+ * 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.store.adapters;
+
+import com.google.common.base.Objects;
+
+/**
+ * Unique key for identifying a plugin. Used for look up and caching.
+ */
+class PluginKey {
+
+ private final String parentName;
+ private final String parentNamespace;
+ private final String artifactName;
+ private final String artifactNamespace;
+ private final String artifactVersion;
+ private final String pluginType;
+ private final String pluginName;
+
+ /**
+ * Creates a {@code PluginKey} with specified plugin identification details.
+ */
+ PluginKey(String parentName, String parentNamespace, String artifactName,
+ String artifactNamespace, String artifactVersion, String pluginType, String pluginName) {
+ this.parentName = parentName;
+ this.parentNamespace = parentNamespace;
+ this.artifactName = artifactName;
+ this.artifactNamespace = artifactNamespace;
+ this.artifactVersion = artifactVersion;
+ this.pluginType = pluginType;
+ this.pluginName = pluginName;
+ }
+
+ /**
+ * Returns the parent name.
+ */
+ public String getParentName() {
+ return parentName;
+ }
+
+ /**
+ * Returns the parent namespace.
+ */
+ public String getParentNamespace() {
+ return parentNamespace;
+ }
+
+ /**
+ * Returns the plugin's artifact name.
+ */
+ public String getArtifactName() {
+ return artifactName;
+ }
+
+ /**
+ * Returns the plugin's artifact namespace.
+ */
+ public String getArtifactNamespace() {
+ return artifactNamespace;
+ }
+
+ /**
+ * Returns the plugin's artifact version.
+ */
+ public String getArtifactVersion() {
+ return artifactVersion;
+ }
+
+ /**
+ * Returns the plugin type.
+ */
+ public String getPluginType() {
+ return pluginType;
+ }
+
+ /**
+ * Returns the plugin name.
+ */
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ /**
+ * Checks equality with another object.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PluginKey)) {
+ return false;
+ }
+ PluginKey pluginKey = (PluginKey) o;
+ return Objects.equal(parentName, pluginKey.parentName)
+ && Objects.equal(parentNamespace, pluginKey.parentNamespace)
+ && Objects.equal(artifactName, pluginKey.artifactName)
+ && Objects.equal(artifactNamespace, pluginKey.artifactNamespace)
+ && Objects.equal(artifactVersion, pluginKey.artifactVersion)
+ && Objects.equal(pluginType, pluginKey.pluginType)
+ && Objects.equal(pluginName, pluginKey.pluginName);
+ }
+
+ /**
+ * Returns a string representation of this key.
+ */
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("parentName", parentName)
+ .add("parentNamespace", parentNamespace)
+ .add("artifactName", artifactName)
+ .add("artifactNamespace", artifactNamespace)
+ .add("artifactVersion", artifactVersion)
+ .add("pluginType", pluginType)
+ .add("pluginName", pluginName)
+ .toString();
+ }
+
+ /**
+ * Computes the hash code for this key.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(parentName, parentNamespace, artifactName, artifactNamespace,
+ artifactVersion, pluginType, pluginName);
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStore.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStore.java
index 2ae2e5f2074b..04c62cf3ab50 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStore.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStore.java
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
+
package io.cdap.cdap.internal.app.store.preview;
import com.google.common.annotations.VisibleForTesting;
@@ -54,17 +55,21 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Default implementation of the {@link PreviewStore} that stores data in a level db table.
*/
public class DefaultPreviewStore implements PreviewStore {
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultPreviewStore.class);
private static final DatasetId PREVIEW_TABLE_ID = NamespaceId.SYSTEM.dataset("preview.table");
private static final DatasetId PREVIEW_QUEUE_TABLE_ID = NamespaceId.SYSTEM.dataset(
"preview.queue.table");
private static final byte[] DATA_ROW_KEY_PREFIX = Bytes.toBytes("dr");
private static final byte[] META_ROW_KEY_PREFIX = Bytes.toBytes("mr");
+ private static final byte[] POLLER_INFO_TO_APP_ID_PREFIX = Bytes.toBytes("p2a");
private static final byte[] TRACER = Bytes.toBytes("t");
private static final byte[] PROPERTY = Bytes.toBytes("p");
private static final byte[] VALUE = Bytes.toBytes("v");
@@ -84,6 +89,10 @@ public class DefaultPreviewStore implements PreviewStore {
private static final byte[] CONFIG = Bytes.toBytes("c");
private static final byte[] APPID = Bytes.toBytes("a");
private static final byte[] PRINCIPAL = Bytes.toBytes("p");
+ private static final Gson ENTITY_GSON = new GsonBuilder().registerTypeAdapter(EntityId.class,
+ new EntityIdTypeAdapter()).create();
+ private static final Gson SCHEMA_GSON = new GsonBuilder().registerTypeAdapter(Schema.class,
+ new SchemaTypeAdapter()).create();
private final AtomicLong counter = new AtomicLong(0L);
@@ -127,9 +136,6 @@ public void put(ApplicationId applicationId, String tracerName, String propertyN
@Override
public Map> get(ApplicationId applicationId, String tracerName) {
- // PreviewStore is a singleton and we have to create gson for each operation since gson is not thread safe.
- Gson gson = new GsonBuilder().registerTypeAdapter(Schema.class, new SchemaTypeAdapter())
- .create();
byte[] startRowKey = getPreviewRowKeyBuilder(DATA_ROW_KEY_PREFIX, applicationId)
.add(tracerName).build().getKey();
byte[] stopRowKey = new MDSKey(Bytes.stopKeyForPrefix(startRowKey)).getKey();
@@ -140,7 +146,7 @@ public Map> get(ApplicationId applicationId, String tr
while ((indexRow = scanner.next()) != null) {
Map columns = indexRow.getColumns();
String propertyName = Bytes.toString(columns.get(PROPERTY));
- JsonElement value = gson.fromJson(Bytes.toString(columns.get(VALUE)), JsonElement.class);
+ JsonElement value = SCHEMA_GSON.fromJson(Bytes.toString(columns.get(VALUE)), JsonElement.class);
List values = result.computeIfAbsent(propertyName, k -> new ArrayList<>());
values.add(value);
}
@@ -167,6 +173,18 @@ private void removePreviewData(byte[] prefix, ApplicationId applicationId) {
@Override
public void remove(ApplicationId applicationId) {
+ byte[] pollerInfo = getPreviewRequestPollerInfo(applicationId);
+ if (pollerInfo != null) {
+ MDSKey indexKey = new MDSKey.Builder().add(POLLER_INFO_TO_APP_ID_PREFIX).add(pollerInfo)
+ .build();
+ try {
+ previewTable.deleteDefaultVersion(indexKey.getKey(), APPID);
+ } catch (IOException e) {
+ // It is ok to not throw exception here, as the data will be eventually cleaned up by the ttl remover.
+ LOG.warn("Failed to delete poller info for application {}", applicationId, e);
+ }
+ }
+
removeFromWaitingState(applicationId);
// remove actual preview user data
removePreviewData(DATA_ROW_KEY_PREFIX, applicationId);
@@ -176,14 +194,11 @@ public void remove(ApplicationId applicationId) {
@Override
public void setProgramId(ProgramRunId programRunId) {
- // PreviewStore is a singleton and we have to create gson for each operation since gson is not thread safe.
- Gson gson = new GsonBuilder().registerTypeAdapter(EntityId.class, new EntityIdTypeAdapter())
- .create();
MDSKey mdsKey = getPreviewRowKeyBuilder(META_ROW_KEY_PREFIX,
programRunId.getParent().getParent()).build();
try {
previewTable.putDefaultVersion(mdsKey.getKey(), RUN,
- Bytes.toBytes(gson.toJson(programRunId)));
+ Bytes.toBytes(ENTITY_GSON.toJson(programRunId)));
} catch (IOException e) {
throw new RuntimeException(String.format("Failed to put %s into preview store", programRunId),
e);
@@ -192,9 +207,6 @@ public void setProgramId(ProgramRunId programRunId) {
@Override
public ProgramRunId getProgramRunId(ApplicationId applicationId) {
- // PreviewStore is a singleton and we have to create gson for each operation since gson is not thread safe.
- Gson gson = new GsonBuilder().registerTypeAdapter(EntityId.class, new EntityIdTypeAdapter())
- .create();
MDSKey mdsKey = getPreviewRowKeyBuilder(META_ROW_KEY_PREFIX, applicationId).build();
byte[] runId = null;
@@ -206,7 +218,7 @@ public ProgramRunId getProgramRunId(ApplicationId applicationId) {
String.format("Failed to get program run id for preview %s", applicationId), e);
}
if (runId != null) {
- return gson.fromJson(Bytes.toString(runId), ProgramRunId.class);
+ return ENTITY_GSON.fromJson(Bytes.toString(runId), ProgramRunId.class);
}
return null;
}
@@ -253,9 +265,6 @@ public PreviewStatus getPreviewStatus(ApplicationId applicationId) {
@Override
public void add(ApplicationId applicationId, AppRequest appRequest,
@Nullable Principal principal) {
- // PreviewStore is a singleton and we have to create gson for each operation since gson is not thread safe.
- Gson gson = new GsonBuilder().registerTypeAdapter(Schema.class, new SchemaTypeAdapter())
- .create();
long timeInSeconds = RunIds.getTime(applicationId.getApplication(), TimeUnit.SECONDS);
MDSKey mdsKey = new MDSKey.Builder()
.add(WAITING)
@@ -266,11 +275,11 @@ public void add(ApplicationId applicationId, AppRequest appRequest,
try {
previewQueueTable.putDefaultVersion(mdsKey.getKey(), APPID,
- Bytes.toBytes(gson.toJson(applicationId)));
+ Bytes.toBytes(SCHEMA_GSON.toJson(applicationId)));
previewQueueTable.putDefaultVersion(mdsKey.getKey(), CONFIG,
- Bytes.toBytes(gson.toJson(appRequest)));
+ Bytes.toBytes(SCHEMA_GSON.toJson(appRequest)));
previewQueueTable.putDefaultVersion(mdsKey.getKey(), PRINCIPAL,
- Bytes.toBytes(gson.toJson(principal)));
+ Bytes.toBytes(SCHEMA_GSON.toJson(principal)));
long submitTimeInMillis = RunIds.getTime(applicationId.getApplication(),
TimeUnit.MILLISECONDS);
setPreviewStatus(applicationId,
@@ -307,9 +316,6 @@ private void removeFromWaitingState(ApplicationId applicationId) {
@Override
public List getAllInWaitingState() {
- // PreviewStore is a singleton and we have to create gson for each operation since gson is not thread safe.
- Gson gson = new GsonBuilder().registerTypeAdapter(Schema.class, new SchemaTypeAdapter())
- .create();
byte[] startRowKey = new MDSKey.Builder().add(WAITING).build().getKey();
byte[] stopRowKey = new MDSKey(Bytes.stopKeyForPrefix(startRowKey)).getKey();
@@ -318,10 +324,10 @@ public List getAllInWaitingState() {
Row indexRow;
while ((indexRow = scanner.next()) != null) {
Map columns = indexRow.getColumns();
- AppRequest request = gson.fromJson(Bytes.toString(columns.get(CONFIG)), AppRequest.class);
- ApplicationId applicationId = gson.fromJson(Bytes.toString(columns.get(APPID)),
+ AppRequest request = SCHEMA_GSON.fromJson(Bytes.toString(columns.get(CONFIG)), AppRequest.class);
+ ApplicationId applicationId = SCHEMA_GSON.fromJson(Bytes.toString(columns.get(APPID)),
ApplicationId.class);
- Principal principal = gson.fromJson(Bytes.toString(columns.get(PRINCIPAL)),
+ Principal principal = SCHEMA_GSON.fromJson(Bytes.toString(columns.get(PRINCIPAL)),
Principal.class);
result.add(new PreviewRequest(applicationId, request, principal));
}
@@ -355,9 +361,6 @@ public void setPreviewRequestPollerInfo(ApplicationId applicationId, @Nullable b
}
private void setPollerinfo(ApplicationId applicationId, byte[] pollerInfo) {
- // PreviewStore is a singleton and we have to create gson for each operation since gson is not thread safe.
- Gson gson = new GsonBuilder().registerTypeAdapter(Schema.class, new SchemaTypeAdapter())
- .create();
MDSKey mdsKey = getPreviewRowKeyBuilder(META_ROW_KEY_PREFIX, applicationId).build();
try {
@@ -365,9 +368,19 @@ private void setPollerinfo(ApplicationId applicationId, byte[] pollerInfo) {
} catch (IOException e) {
String msg = String.format(
"Error while setting the poller information %s for waiting preview application %s.",
- gson.toJson(pollerInfo), applicationId);
+ SCHEMA_GSON.toJson(pollerInfo), applicationId);
throw new RuntimeException(msg, e);
}
+
+ // Add an entry to the index: pollerInfo -> applicationId
+ MDSKey indexKey = new MDSKey.Builder().add(POLLER_INFO_TO_APP_ID_PREFIX).add(pollerInfo)
+ .build();
+ try {
+ previewTable.putDefaultVersion(indexKey.getKey(), APPID,
+ Bytes.toBytes(SCHEMA_GSON.toJson(applicationId)));
+ } catch (IOException e) {
+ throw new RuntimeException("Error while creating poller info index.", e);
+ }
}
@Override
@@ -381,17 +394,25 @@ public byte[] getPreviewRequestPollerInfo(ApplicationId applicationId) {
throw new RuntimeException(
String.format("Failed to get the poller info for preview %s", applicationId), e);
}
- if (pollerInfo != null) {
- return pollerInfo;
- }
+ return pollerInfo;
+ }
- return null;
+ public ApplicationId getApplicationId(byte[] pollerInfo) {
+ MDSKey indexKey = new MDSKey.Builder().add(POLLER_INFO_TO_APP_ID_PREFIX).add(pollerInfo)
+ .build();
+ try {
+ byte[] applicationIdBytes = previewTable.getDefaultVersion(indexKey.getKey(), APPID);
+ if (applicationIdBytes == null) {
+ return null;
+ }
+ return SCHEMA_GSON.fromJson(Bytes.toString(applicationIdBytes), ApplicationId.class);
+ } catch (IOException e) {
+ throw new RuntimeException("Error while getting application id for poller info.", e);
+ }
}
@Override
public void deleteExpiredData(long ttlInSeconds) {
- Gson gson = new GsonBuilder().registerTypeAdapter(EntityId.class, new EntityIdTypeAdapter())
- .create();
byte[] startRowKey = new MDSKey.Builder().add(META_ROW_KEY_PREFIX).build().getKey();
byte[] stopRowKey = new MDSKey(Bytes.stopKeyForPrefix(startRowKey)).getKey();
@@ -405,7 +426,7 @@ public void deleteExpiredData(long ttlInSeconds) {
continue;
}
- ApplicationId applicationId = gson.fromJson(applicationIdGson, ApplicationId.class);
+ ApplicationId applicationId = ENTITY_GSON.fromJson(applicationIdGson, ApplicationId.class);
long applicationSubmitTime = RunIds.getTime(applicationId.getApplication(),
TimeUnit.SECONDS);
if ((currentTimeInSeconds - applicationSubmitTime) > ttlInSeconds) {
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerService.java
index 1397263d7cf0..af0d98dd81c0 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerService.java
@@ -30,11 +30,15 @@
import io.cdap.cdap.common.http.CommonNettyHttpServiceFactory;
import io.cdap.cdap.common.internal.remote.TaskWorkerHttpHandlerInternal;
import io.cdap.cdap.common.security.HttpsEnabler;
+import io.cdap.cdap.gateway.handlers.PingHandler;
import io.cdap.http.ChannelPipelineModifier;
+import io.cdap.http.HttpHandler;
import io.cdap.http.NettyHttpService;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpContentDecompressor;
import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.twill.common.Cancellable;
import org.apache.twill.discovery.DiscoveryService;
@@ -70,6 +74,12 @@ public class TaskWorkerService extends AbstractIdleService {
cConf.set(TaskWorker.WORK_DIR, workDir);
}
+ List handlers = Arrays.asList(
+ new PingHandler(),
+ new TaskWorkerHttpHandlerInternal(cConf, discoveryService,
+ discoveryServiceClient, this::stopService,
+ metricsCollectionService));
+
NettyHttpService.Builder builder = commonNettyHttpServiceFactory.builder(
Constants.Service.TASK_WORKER)
.setHost(cConf.get(Constants.TaskWorker.ADDRESS))
@@ -83,9 +93,7 @@ public void modify(ChannelPipeline pipeline) {
pipeline.addAfter("compressor", "decompressor", new HttpContentDecompressor());
}
})
- .setHttpHandlers(new TaskWorkerHttpHandlerInternal(cConf, discoveryService,
- discoveryServiceClient, this::stopService,
- metricsCollectionService));
+ .setHttpHandlers(handlers);
if (cConf.getBoolean(Constants.Security.SSL.INTERNAL_ENABLED)) {
new HttpsEnabler().configureKeyStore(cConf, sConf).enable(builder);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceLauncher.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceLauncher.java
index a776ee09f688..8f43c9697366 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceLauncher.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceLauncher.java
@@ -269,6 +269,14 @@ public void run() {
}
}
+ if (cConf.getBoolean(TaskWorker.TASK_WORKER_PROBE_ENABLED)) {
+ if (twillPreparer instanceof ExtendedTwillPreparer) {
+ twillPreparer = ((ExtendedTwillPreparer) twillPreparer)
+ .addProbes(TaskWorkerTwillRunnable.class.getSimpleName(),
+ cConf.getPropsWithPrefix(TaskWorker.TASK_WORKER_PROBE_PREFIX));
+ }
+ }
+
// Set JVM options for task worker and artifact localizer
twillPreparer.setJVMOptions(TaskWorkerTwillRunnable.class.getSimpleName(),
cConf.get(Constants.TaskWorker.CONTAINER_JVM_OPTS));
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerTwillRunnable.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerTwillRunnable.java
index 8853bdd7cbd6..6ae74058e12e 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerTwillRunnable.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/TaskWorkerTwillRunnable.java
@@ -45,7 +45,7 @@
import io.cdap.cdap.logging.guice.RemoteLogAppenderModule;
import io.cdap.cdap.master.environment.MasterEnvironments;
import io.cdap.cdap.master.spi.environment.MasterEnvironment;
-import io.cdap.cdap.messaging.guice.MessagingServiceModule;
+import io.cdap.cdap.messaging.guice.client.TaskWorkerMessagingClientModule;
import io.cdap.cdap.metrics.guice.MetricsClientRuntimeModule;
import io.cdap.cdap.proto.id.NamespaceId;
import io.cdap.cdap.security.auth.context.AuthenticationContextModules;
@@ -95,7 +95,7 @@ static Injector createInjector(CConfiguration cConf, Configuration hConf) {
modules.add(new IOModule());
modules.add(new AuthenticationContextModules().getMasterWorkerModule());
modules.add(coreSecurityModule);
- modules.add(new MessagingServiceModule(cConf));
+ modules.add(new TaskWorkerMessagingClientModule(cConf));
modules.add(new SystemAppModule());
modules.add(new MetricsClientRuntimeModule().getDistributedModules());
modules.add(new AuditLogWriterModule(cConf).getDistributedModules());
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/system/SystemWorkerTwillRunnable.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/system/SystemWorkerTwillRunnable.java
index b8d0db7543e8..b45767ded47c 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/system/SystemWorkerTwillRunnable.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/app/worker/system/SystemWorkerTwillRunnable.java
@@ -33,6 +33,7 @@
import io.cdap.cdap.api.artifact.ArtifactManager;
import io.cdap.cdap.api.metrics.MetricsCollectionService;
import io.cdap.cdap.app.guice.AppFabricServiceRuntimeModule;
+import io.cdap.cdap.app.guice.AppFabricServiceRuntimeModule.ServiceType;
import io.cdap.cdap.app.guice.AuditLogWriterModule;
import io.cdap.cdap.app.guice.AuthorizationModule;
import io.cdap.cdap.app.guice.DistributedArtifactManagerModule;
@@ -91,6 +92,7 @@
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -156,7 +158,8 @@ protected void bindKeyManager(Binder binder) {
new AuthorizationModule(),
new AuthorizationEnforcementModule().getMasterModule(),
new AuditLogWriterModule(cConf).getDistributedModules(),
- Modules.override(new AppFabricServiceRuntimeModule(cConf).getDistributedModules())
+ Modules.override(new AppFabricServiceRuntimeModule(cConf, AppFabricServiceRuntimeModule.ALL_SERVICE_TYPES)
+ .getDistributedModules())
.with(new AbstractModule() {
// To enable localisation of artifacts
@Override
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/BootstrapService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/BootstrapService.java
index c7f929ee0897..70f95fe93b77 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/BootstrapService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/BootstrapService.java
@@ -82,6 +82,13 @@ public class BootstrapService extends AbstractIdleService {
protected void startUp() {
LOG.info("Starting {}", getClass().getSimpleName());
config = bootstrapConfigProvider.getConfig();
+
+ // Do not start other services if no bootstrap steps were executed.
+ if (config.getSteps().isEmpty()) {
+ LOG.info("Skipping execution of bootstrap steps as bootstrap config has no steps.");
+ return;
+ }
+
executorService = Executors.newSingleThreadExecutor(
Threads.createDaemonThreadFactory("bootstrap-service"));
executorService.execute(() -> {
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/guice/BootstrapModules.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/guice/BootstrapModules.java
index d2ca74ac73fe..36e4528eae02 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/guice/BootstrapModules.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/bootstrap/guice/BootstrapModules.java
@@ -72,6 +72,23 @@ protected void configure() {
};
}
+ /**
+ * Module with empty config to not perform any bootstrap steps.
+ *
+ * @return bootstrap module that do not need to execute bootstrap steps.
+ */
+ public static Module getNoOpModule() {
+ return new BaseModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ BootstrapConfigProvider inMemoryProvider = new InMemoryBootstrapConfigProvider(
+ BootstrapConfig.EMPTY);
+ bind(BootstrapConfigProvider.class).toInstance(inMemoryProvider);
+ }
+ };
+ }
+
/**
* Bindings common to all modules
*/
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/main/java/io/cdap/cdap/internal/events/StartProgramEventSubscriber.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/events/StartProgramEventSubscriber.java
index e88502095182..2816dd10429c 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/events/StartProgramEventSubscriber.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/events/StartProgramEventSubscriber.java
@@ -21,7 +21,7 @@
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.internal.app.services.ProgramLifecycleService;
-import io.cdap.cdap.internal.app.services.RunRecordMonitorService;
+import io.cdap.cdap.internal.app.services.FlowControlService;
import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.id.ProgramReference;
import io.cdap.cdap.proto.id.ProgramRunId;
@@ -54,7 +54,7 @@ public class StartProgramEventSubscriber extends EventSubscriber {
private final CConfiguration cConf;
private final EventReaderProvider extensionProvider;
private final ProgramLifecycleService lifecycleService;
- private final RunRecordMonitorService runRecordMonitorService;
+ private final FlowControlService flowControlService;
private ScheduledExecutorService executor;
private Collection> readers;
private ExecutorService threadPoolExecutor;
@@ -66,17 +66,17 @@ public class StartProgramEventSubscriber extends EventSubscriber {
* @param cConf CDAP configuration
* @param extensionProvider eventReaderProvider for StartProgramEvent Readers
* @param lifecycleService to publish start programs to TMS
- * @param runRecordMonitorService basic flow-control
+ * @param flowControlService basic flow-control
*/
@Inject
StartProgramEventSubscriber(CConfiguration cConf,
EventReaderProvider extensionProvider,
ProgramLifecycleService lifecycleService,
- RunRecordMonitorService runRecordMonitorService) {
+ FlowControlService flowControlService) {
this.cConf = cConf;
this.extensionProvider = extensionProvider;
this.lifecycleService = lifecycleService;
- this.runRecordMonitorService = runRecordMonitorService;
+ this.flowControlService = flowControlService;
maxConcurrentRuns = -1;
}
@@ -132,14 +132,14 @@ protected void runOneIteration() throws Exception {
if (threadPoolExecutor != null) {
for (EventReader reader : readers) {
threadPoolExecutor.execute(() -> {
- if (runRecordMonitorService.isRunning()) {
+ if (flowControlService.isRunning()) {
// Only attempt to process event if there is no max or the current count is less than max
if (hasNominalCapacity()) {
processEvents(reader);
}
} else {
- LOG.warn("RunRecordMonitorService not yet running, currently in state: {}."
- + " Status will be checked again in next attempt.", runRecordMonitorService.state());
+ LOG.warn("FlowControlService not yet running, currently in state: {}."
+ + " Status will be checked again in next attempt.", flowControlService.state());
}
});
}
@@ -153,7 +153,7 @@ protected void runOneIteration() throws Exception {
*/
@VisibleForTesting
boolean hasNominalCapacity() {
- RunRecordMonitorService.Counter counter = runRecordMonitorService.getCount();
+ FlowControlService.Counter counter = flowControlService.getCounter();
// no limit
if (maxConcurrentRuns <= 0) {
return true;
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/operation/OperationNotificationSubscriberService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/operation/OperationNotificationSubscriberService.java
index 04f9dd5eecf9..ae20897617f2 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/operation/OperationNotificationSubscriberService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/operation/OperationNotificationSubscriberService.java
@@ -76,19 +76,15 @@ protected void startUp() throws Exception {
RetryStrategy retryStrategy =
RetryStrategies.fromConfiguration(cConf, Constants.Service.RUNTIME_MONITOR_RETRY_PREFIX);
- TransactionRunners.run(
- transactionRunner, context -> {
- Retries.runWithRetries(
- () -> {
- processStartingOperations(context);
- processRunningOperations(context);
- processStoppingOperations(context);
- },
- retryStrategy,
- e -> true
- );
- }
- );
+ Retries.runWithRetries(() ->
+ TransactionRunners.run(transactionRunner, context -> {
+ processStartingOperations(context);
+ processRunningOperations(context);
+ processStoppingOperations(context);
+ }),
+ retryStrategy,
+ e -> true);
+
List children = new ArrayList<>();
String topicPrefix = cConf.get(Constants.Operation.STATUS_EVENT_TOPIC);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/DefaultProvisionerContext.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/DefaultProvisionerContext.java
index 182ba3f8da19..f77b9d39f4cb 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/DefaultProvisionerContext.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/DefaultProvisionerContext.java
@@ -16,6 +16,8 @@
package io.cdap.cdap.internal.provision;
+import io.cdap.cdap.api.exception.ErrorCategory;
+import io.cdap.cdap.api.exception.ErrorCategory.ErrorCategoryEnum;
import io.cdap.cdap.api.metrics.MetricsCollectionService;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.common.logging.LoggingContext;
@@ -62,6 +64,7 @@ public class DefaultProvisionerContext implements ProvisionerContext {
private final String profileName;
private final LoggingContext loggingContext;
private final Executor executor;
+ private final ErrorCategory errorCategory;
DefaultProvisionerContext(ProgramRunId programRunId, String provisionerName,
Map properties,
@@ -69,7 +72,7 @@ public class DefaultProvisionerContext implements ProvisionerContext {
@Nullable VersionInfo appCDAPVersion, LocationFactory locationFactory,
RuntimeMonitorType runtimeMonitorType, MetricsCollectionService metricsCollectionService,
@Nullable String profileName, Executor executor,
- LoggingContext loggingContext) {
+ LoggingContext loggingContext, ErrorCategory errorCategory) {
this.programRun = new ProgramRun(programRunId.getNamespace(), programRunId.getApplication(),
programRunId.getProgram(), programRunId.getRun());
this.programRunInfo = new ProgramRunInfo.Builder()
@@ -92,6 +95,7 @@ public class DefaultProvisionerContext implements ProvisionerContext {
this.metricsCollectionService = metricsCollectionService;
this.provisionerName = provisionerName;
this.executor = executor;
+ this.errorCategory = errorCategory;
}
@Override
@@ -146,6 +150,11 @@ public String getProfileName() {
return profileName;
}
+ @Override
+ public ErrorCategory getErrorCategory() {
+ return errorCategory == null ? new ErrorCategory(ErrorCategoryEnum.OTHERS) : errorCategory;
+ }
+
@Override
public ProvisionerMetrics getMetrics(Map context) {
Map tags = new HashMap<>(context);
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerStore.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerStore.java
index 6212b2bdf7dc..ae269763a04d 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerStore.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerStore.java
@@ -16,31 +16,42 @@
package io.cdap.cdap.internal.provision;
+import com.google.common.collect.Lists;
+import io.cdap.cdap.api.dataset.lib.CloseableIterator;
+import io.cdap.cdap.internal.provision.adapters.ProvisioningTaskInfoAdapter;
import io.cdap.cdap.proto.id.ProgramRunId;
+import io.cdap.cdap.spi.data.StructuredRow;
+import io.cdap.cdap.spi.data.StructuredTable;
import io.cdap.cdap.spi.data.StructuredTableContext;
import io.cdap.cdap.spi.data.TableNotFoundException;
+import io.cdap.cdap.spi.data.table.field.Field;
+import io.cdap.cdap.spi.data.table.field.Fields;
+import io.cdap.cdap.spi.data.table.field.Range;
import io.cdap.cdap.spi.data.transaction.TransactionRunner;
import io.cdap.cdap.spi.data.transaction.TransactionRunners;
+import io.cdap.cdap.store.StoreDefinition;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Inject;
/**
* Stores information used for provisioning.
*
- * Stores subscriber offset information for TMS, cluster information for program runs, and state
- * information for each provision and deprovision operation.
+ * Stores subscriber offset information for TMS, cluster information for program runs, and state
+ * information for each provision and deprovision operation.
*
- * Provisioner Store uses transactionRunners to perform underlying CRUD operations.
+ * Provisioner Store uses transactionRunners to perform underlying CRUD operations.
*/
-final class ProvisionerStore {
+public final class ProvisionerStore {
private final TransactionRunner txRunner;
- private ProvisionerTable getProvisionerTable(StructuredTableContext context)
+ private StructuredTable getProvisionerTable(StructuredTableContext context)
throws TableNotFoundException {
- return new ProvisionerTable(context);
+ return context.getTable(StoreDefinition.ProvisionerStore.PROVISIONER_TABLE);
}
@Inject
@@ -48,49 +59,126 @@ private ProvisionerTable getProvisionerTable(StructuredTableContext context)
this.txRunner = txRunner;
}
+ /**
+ * @return List of {@link ProvisioningTaskInfo}
+ * @throws IOException if there is an error reading from underlying structured table.
+ */
List listTaskInfo() throws IOException {
return TransactionRunners.run(txRunner, context -> {
- return getProvisionerTable(context).listTaskInfo();
+ List result = new ArrayList<>();
+ try (CloseableIterator iterator = getProvisionerTable(context).scan(
+ Range.all(), Integer.MAX_VALUE)) {
+ while (iterator.hasNext()) {
+ StructuredRow row = iterator.next();
+ result.add(ProvisioningTaskInfoAdapter.fromJson(
+ row.getString(StoreDefinition.ProvisionerStore.PROVISIONER_TASK_INFO_FIELD),
+ row.getString(StoreDefinition.ProvisionerStore.NAMESPACE_FIELD), context));
+ }
+ }
+ return result;
}, IOException.class);
}
+ /**
+ * Fetch Provisioning Task Information.
+ *
+ * @param key ProvisioningTaskKey for the corresponding task info.
+ * @return instance of {@link ProvisioningTaskInfo}.
+ * @throws IOException if there is an issue reading from underlying structured table.
+ */
@Nullable
- ProvisioningTaskInfo getTaskInfo(final ProvisioningTaskKey key) throws IOException {
+ public ProvisioningTaskInfo getTaskInfo(final ProvisioningTaskKey key) throws IOException {
return TransactionRunners.run(txRunner, context -> {
- return getProvisionerTable(context).getTaskInfo(key);
+ return fetchTaskInfo(context, key);
}, IOException.class);
}
- void putTaskInfo(final ProvisioningTaskInfo taskInfo) throws IOException {
+ /**
+ * Persist the provisioning taskInfo.
+ *
+ * @param taskInfo {@link ProvisioningTaskInfo}to be persisted.
+ * @throws IOException if there is an issue writing to the underlying structured table.
+ */
+ public void putTaskInfo(final ProvisioningTaskInfo taskInfo) throws IOException {
TransactionRunners.run(txRunner, context -> {
- getProvisionerTable(context).putTaskInfo(taskInfo);
+ persistTaskInfo(context, taskInfo);
}, IOException.class);
}
+ /**
+ * Delete provisioning task info for the corresponding program run id.
+ *
+ * @param runId to delete.
+ * @throws IOException if there is an issue deleting from the underlying structured table.
+ */
void deleteTaskInfo(ProgramRunId programRunId) throws IOException {
TransactionRunners.run(txRunner, context -> {
- getProvisionerTable(context).deleteTaskInfo(programRunId);
+ // Delete the keys with Provision and Deprovision, type is set to null to delete provision and deprovision types
+ getProvisionerTable(context).deleteAll(Range.singleton(createPrimaryKey(programRunId, null)));
}, IOException.class);
}
@Nullable
ProvisioningTaskInfo getExistingAndCancel(final ProvisioningTaskKey taskKey) throws IOException {
return TransactionRunners.run(txRunner, context -> {
- ProvisionerTable table = getProvisionerTable(context);
- ProvisioningTaskInfo currentTaskInfo = table.getTaskInfo(taskKey);
+ ProvisioningTaskInfo currentTaskInfo = fetchTaskInfo(context, taskKey);
if (currentTaskInfo == null) {
return null;
}
// write that the state has been cancelled. This is in case CDAP dies or is killed before the cluster can
// be deprovisioned and the task state cleaned up. When CDAP starts back up, it will see that the task is
// cancelled and will not resume the task.
- ProvisioningOp newOp =
- new ProvisioningOp(currentTaskInfo.getProvisioningOp().getType(),
- ProvisioningOp.Status.CANCELLED);
+ ProvisioningOp newOp = new ProvisioningOp(currentTaskInfo.getProvisioningOp().getType(),
+ ProvisioningOp.Status.CANCELLED);
ProvisioningTaskInfo newTaskInfo = new ProvisioningTaskInfo(currentTaskInfo, newOp,
currentTaskInfo.getCluster());
- table.putTaskInfo(newTaskInfo);
+ persistTaskInfo(context, newTaskInfo);
return currentTaskInfo;
}, IOException.class);
}
+
+ private List> createPrimaryKey(ProgramRunId runId, @Nullable ProvisioningOp.Type type) {
+ List> fields = Lists.newArrayList(
+ Fields.stringField(StoreDefinition.ProvisionerStore.NAMESPACE_FIELD, runId.getNamespace()),
+ Fields.stringField(StoreDefinition.ProvisionerStore.APPLICATION_FIELD,
+ runId.getApplication()),
+ Fields.stringField(StoreDefinition.ProvisionerStore.VERSION_FIELD, runId.getVersion()),
+ Fields.stringField(StoreDefinition.ProvisionerStore.PROGRAM_TYPE_FIELD,
+ runId.getType().name()),
+ Fields.stringField(StoreDefinition.ProvisionerStore.PROGRAM_FIELD, runId.getProgram()),
+ Fields.stringField(StoreDefinition.ProvisionerStore.RUN_FIELD, runId.getRun()));
+
+ if (null != type) {
+ fields.add(Fields.stringField(StoreDefinition.ProvisionerStore.KEY_TYPE, type.name()));
+ }
+ return fields;
+ }
+
+ /**
+ * Persists {@link ProvisioningTaskInfo} in the provisioner table.
+ */
+ private void persistTaskInfo(StructuredTableContext context, ProvisioningTaskInfo taskInfo)
+ throws IOException {
+ String serializedTaskInfo = ProvisioningTaskInfoAdapter.toJson(taskInfo, context);
+ List> fields = createPrimaryKey(taskInfo.getTaskKey().getProgramRunId(),
+ taskInfo.getTaskKey().getType());
+ fields.add(Fields.stringField(StoreDefinition.ProvisionerStore.PROVISIONER_TASK_INFO_FIELD,
+ serializedTaskInfo));
+ getProvisionerTable(context).upsert(fields);
+ }
+
+ /**
+ * Fetches {@link ProvisioningTaskInfo} from the provisioner table.
+ */
+ private ProvisioningTaskInfo fetchTaskInfo(StructuredTableContext context,
+ ProvisioningTaskKey key) throws IOException {
+ Optional row = getProvisionerTable(context).read(
+ createPrimaryKey(key.getProgramRunId(), key.getType()));
+ String taskInfoJson = row.map(structuredRow -> structuredRow.getString(
+ StoreDefinition.ProvisionerStore.PROVISIONER_TASK_INFO_FIELD)).orElse(null);
+ String namespace = row.map(
+ structuredRow -> structuredRow.getString(StoreDefinition.ProvisionerStore.NAMESPACE_FIELD))
+ .orElse(null);
+ return ProvisioningTaskInfoAdapter.fromJson(taskInfoJson, namespace, context);
+ }
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerTable.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerTable.java
deleted file mode 100644
index aa311c6e362c..000000000000
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisionerTable.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright © 2019 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.provision;
-
-import com.google.common.collect.Lists;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import io.cdap.cdap.api.dataset.lib.CloseableIterator;
-import io.cdap.cdap.app.runtime.Arguments;
-import io.cdap.cdap.app.runtime.ProgramOptions;
-import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter;
-import io.cdap.cdap.internal.app.runtime.codec.ArgumentsCodec;
-import io.cdap.cdap.internal.app.runtime.codec.ProgramOptionsCodec;
-import io.cdap.cdap.proto.id.ProgramRunId;
-import io.cdap.cdap.spi.data.StructuredRow;
-import io.cdap.cdap.spi.data.StructuredTable;
-import io.cdap.cdap.spi.data.StructuredTableContext;
-import io.cdap.cdap.spi.data.TableNotFoundException;
-import io.cdap.cdap.spi.data.table.field.Field;
-import io.cdap.cdap.spi.data.table.field.Fields;
-import io.cdap.cdap.spi.data.table.field.Range;
-import io.cdap.cdap.store.StoreDefinition;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import javax.annotation.Nullable;
-
-/**
- * Operations on top of StructuredTable for Provisioning related CRUD operations
- */
-public class ProvisionerTable {
-
- private final StructuredTable table;
- private static final Gson GSON = ApplicationSpecificationAdapter.addTypeAdapters(
- new GsonBuilder())
- .registerTypeAdapter(ProgramOptions.class, new ProgramOptionsCodec())
- .registerTypeAdapter(Arguments.class, new ArgumentsCodec())
- .create();
-
- public ProvisionerTable(StructuredTableContext context) throws TableNotFoundException {
- this.table = context.getTable(StoreDefinition.ProvisionerStore.PROVISIONER_TABLE);
- }
-
- /**
- * @return List of {@link ProvisioningTaskInfo}
- * @throws IOException if there is an error reading from underlying structured table.
- */
- public List listTaskInfo() throws IOException {
- List result;
- try (CloseableIterator iterator = table.scan(Range.all(), Integer.MAX_VALUE)) {
- result = new ArrayList<>();
- while (iterator.hasNext()) {
- result.add(
- deserialize(iterator.next()
- .getString(StoreDefinition.ProvisionerStore.PROVISIONER_TASK_INFO_FIELD))
- );
- }
- }
- return result;
- }
-
- /**
- * Fetch Provisioning Task Information
- *
- * @param key ProvisioningTaskKey for the corresponding task info.
- * @return instance of {@link ProvisioningTaskInfo}.
- * @throws IOException if there is an issue reading from underlying structured table.
- */
- @Nullable
- public ProvisioningTaskInfo getTaskInfo(ProvisioningTaskKey key) throws IOException {
- Optional row = table.read(
- createPrimaryKey(key.getProgramRunId(), key.getType()));
- return row.isPresent()
- ? deserialize(
- row.get().getString(StoreDefinition.ProvisionerStore.PROVISIONER_TASK_INFO_FIELD)) :
- null;
- }
-
- /**
- * Persist the provisioning taskInfo.
- *
- * @param taskInfo {@link ProvisioningTaskInfo}to be persisted.
- * @throws IOException if there is an issue writing to the underlying structured table.
- */
- public void putTaskInfo(ProvisioningTaskInfo taskInfo) throws IOException {
- List> fields = createPrimaryKey(taskInfo.getTaskKey().getProgramRunId(),
- taskInfo.getTaskKey().getType());
- fields.add(Fields.stringField(StoreDefinition.ProvisionerStore.PROVISIONER_TASK_INFO_FIELD,
- serialize(taskInfo)));
- table.upsert(fields);
- }
-
- /**
- * Delete provisioning task info for the corresponding program run id.
- *
- * @param runId to delete.
- * @throws IOException if there is an issue deleting from the underlying structured table.
- */
- public void deleteTaskInfo(ProgramRunId runId) throws IOException {
- // Delete the keys with Provision and Deprovision, type is set to null to delete provision and deprovision types
- table.deleteAll(Range.singleton(createPrimaryKey(runId, null)));
- }
-
- private List> createPrimaryKey(ProgramRunId runId, @Nullable ProvisioningOp.Type type) {
- List> fields = Lists.newArrayList(
- Fields.stringField(StoreDefinition.ProvisionerStore.NAMESPACE_FIELD, runId.getNamespace()),
- Fields.stringField(StoreDefinition.ProvisionerStore.APPLICATION_FIELD,
- runId.getApplication()),
- Fields.stringField(StoreDefinition.ProvisionerStore.VERSION_FIELD, runId.getVersion()),
- Fields.stringField(StoreDefinition.ProvisionerStore.PROGRAM_TYPE_FIELD,
- runId.getType().name()),
- Fields.stringField(StoreDefinition.ProvisionerStore.PROGRAM_FIELD, runId.getProgram()),
- Fields.stringField(StoreDefinition.ProvisionerStore.RUN_FIELD, runId.getRun()));
-
- if (null != type) {
- fields.add(Fields.stringField(StoreDefinition.ProvisionerStore.KEY_TYPE, type.name()));
- }
- return fields;
- }
-
- private ProvisioningTaskInfo deserialize(String provisioningTaskInfo) {
- return GSON.fromJson(provisioningTaskInfo, ProvisioningTaskInfo.class);
- }
-
- private String serialize(ProvisioningTaskInfo taskInfo) {
- return GSON.toJson(taskInfo, ProvisioningTaskInfo.class);
- }
-}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisioningService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisioningService.java
index 8fbed80b5bbb..a23e15788f96 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisioningService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/ProvisioningService.java
@@ -23,6 +23,11 @@
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
+import io.cdap.cdap.api.exception.ErrorCategory;
+import io.cdap.cdap.api.exception.ErrorCategory.ErrorCategoryEnum;
+import io.cdap.cdap.api.exception.ErrorType;
+import io.cdap.cdap.api.exception.ErrorUtils;
+import io.cdap.cdap.api.exception.ProgramFailureException;
import io.cdap.cdap.api.macro.InvalidMacroException;
import io.cdap.cdap.api.macro.MacroEvaluator;
import io.cdap.cdap.api.macro.MacroParserOptions;
@@ -112,7 +117,8 @@
/**
* Service for provisioning related operations.
*
- * TODO (CDAP-21111): Split ProvisioningService for Appfabric Server and Appfabric Processor.
+ * TODO (CDAP-21111): Split ProvisioningService for Appfabric Server and Appfabric
+ * Processor.
*/
public class ProvisioningService extends AbstractIdleService {
@@ -197,10 +203,10 @@ protected void shutDown() throws Exception {
/**
* Returns the {@link ClusterStatus} for the cluster being used to execute the given program run.
*
- * @param programRunId the program run id for checking the cluster status
+ * @param programRunId the program run id for checking the cluster status
* @param programOptions the program options for the given run
- * @param cluster the {@link Cluster} information for the given run
- * @param userId the user id to use for {@link SecureStore} operation.
+ * @param cluster the {@link Cluster} information for the given run
+ * @param userId the user id to use for {@link SecureStore} operation.
* @return the {@link ClusterStatus}
* @throws Exception if non-retryable exception is encountered when querying cluster status
*/
@@ -226,7 +232,7 @@ public ClusterStatus getClusterStatus(ProgramRunId programRunId, ProgramOptions
Networks.getAddress(cConf, Constants.NETWORK_PROXY_ADDRESS), null, null);
}
context = createContext(cConf, programOptions, programRunId, userId, properties,
- defaultSSHContext);
+ defaultSSHContext, new ErrorCategory(ErrorCategoryEnum.OTHERS));
} catch (InvalidMacroException e) {
// This shouldn't happen
runWithProgramLogging(programRunId, systemArgs,
@@ -351,7 +357,7 @@ Optional cancelDeprovisionTask(ProgramRunId programRunId)
* using their own executor.
*
* @param provisionRequest the provision request
- * @param context context for the transaction
+ * @param context context for the transaction
* @return runnable that will actually execute the cluster provisioning
*/
public Runnable provision(ProvisionRequest provisionRequest, StructuredTableContext context)
@@ -363,15 +369,18 @@ public Runnable provision(ProvisionRequest provisionRequest, StructuredTableCont
Map args = programOptions.getArguments().asMap();
String name = SystemArguments.getProfileProvisioner(args);
Provisioner provisioner = provisionerInfo.get().provisioners.get(name);
+ ErrorCategory errorCategory = new ErrorCategory(ErrorCategoryEnum.PROVISIONING);
// any errors seen here will transition the state straight to deprovisioned since no cluster create was attempted
if (provisioner == null) {
+ String errorMessage = String.format("Could not provision cluster for the run because "
+ + "provisioner %s does not exist.", name);
+ String errorReason = String.format("Provisioner %s does not exist.", name);
+ ProgramFailureException ex = ErrorUtils.getProgramFailureException(errorCategory, errorReason,
+ errorMessage, ErrorType.SYSTEM, false, null);
runWithProgramLogging(
programRunId, args,
- () -> LOG.error(
- "Could not provision cluster for the run because provisioner {} does not exist.",
- name));
- programStateWriter.error(programRunId,
- new IllegalStateException("Provisioner does not exist."));
+ () -> LOG.error(errorMessage, ex));
+ programStateWriter.error(programRunId, ex);
provisionerNotifier.deprovisioned(programRunId);
return () -> {
};
@@ -385,15 +394,17 @@ public Runnable provision(ProvisionRequest provisionRequest, StructuredTableCont
Set unfulfilledRequirements =
getUnfulfilledRequirements(provisioner.getCapabilities(), requirements);
if (!unfulfilledRequirements.isEmpty()) {
+ String errorMessage = String.format("'%s' cannot be run using profile '%s' because "
+ + "the profile does not met all plugin requirements. Following requirements "
+ + "were not meet by the listed plugins: '%s'", programRunId.getProgram(), name,
+ groupByRequirement(unfulfilledRequirements));
+ String errorReason = String.format("Provisioner %s does not meet all the requirements for "
+ + "the program %s to run.", name, programRunId.getProgram());
+ ProgramFailureException ex = ErrorUtils.getProgramFailureException(errorCategory, errorReason,
+ errorMessage, ErrorType.SYSTEM, false, null);
runWithProgramLogging(programRunId, args, () ->
- LOG.error(String.format(
- "'%s' cannot be run using profile '%s' because the profile does not met all "
- + "plugin requirements. Following requirements were not meet by the listed "
- + "plugins: '%s'", programRunId.getProgram(), name,
- groupByRequirement(unfulfilledRequirements))));
- programStateWriter.error(programRunId,
- new IllegalArgumentException("Provisioner does not meet all the "
- + "requirements for the program to run."));
+ LOG.error(errorMessage, ex));
+ programStateWriter.error(programRunId, ex);
provisionerNotifier.deprovisioned(programRunId);
return () -> {
};
@@ -408,8 +419,7 @@ public Runnable provision(ProvisionRequest provisionRequest, StructuredTableCont
programOptions,
properties, name, provisionRequest.getUser(), provisioningOp,
createKeysDirectory(programRunId).toURI(), null);
- ProvisionerTable provisionerTable = new ProvisionerTable(context);
- provisionerTable.putTaskInfo(provisioningTaskInfo);
+ provisionerStore.putTaskInfo(provisioningTaskInfo);
return createProvisionTask(provisioningTaskInfo, provisioner);
}
@@ -428,7 +438,7 @@ public Optional getRuntimeJobManager(ProgramRunId programRunI
String user = programOptions.getArguments().getOption(ProgramOptionConstants.USER_ID);
Map properties = SystemArguments.getProfileProperties(systemArgs);
ProvisionerContext context = createContext(cConf, programOptions, programRunId, user,
- properties, null);
+ properties, null, new ErrorCategory(ErrorCategoryEnum.OTHERS));
return provisioner.getRuntimeJobManager(context)
.map(manager ->
new RuntimeJobManagerCallWrapper(provisioner.getClass().getClassLoader(), manager));
@@ -496,8 +506,7 @@ Runnable deprovision(ProgramRunId programRunId, StructuredTableContext context,
ProvisioningOp.Status.REQUESTING_DELETE);
ProvisioningTaskInfo provisioningTaskInfo = new ProvisioningTaskInfo(existing, provisioningOp,
existing.getCluster());
- ProvisionerTable provisionerTable = new ProvisionerTable(context);
- provisionerTable.putTaskInfo(provisioningTaskInfo);
+ provisionerStore.putTaskInfo(provisioningTaskInfo);
return createDeprovisionTask(provisioningTaskInfo, provisioner, taskCleanup);
}
@@ -665,6 +674,7 @@ private Runnable createProvisionTask(ProvisioningTaskInfo taskInfo, Provisioner
ProgramRunId programRunId = taskInfo.getProgramRunId();
ProgramOptions programOptions = taskInfo.getProgramOptions();
Map systemArgs = programOptions.getArguments().asMap();
+ ErrorCategory errorCategory = new ErrorCategory(ErrorCategoryEnum.PROVISIONING);
ProvisionerContext context;
try {
SSHContext sshContext = new DefaultSSHContext(
@@ -672,21 +682,29 @@ private Runnable createProvisionTask(ProvisioningTaskInfo taskInfo, Provisioner
locationFactory.create(taskInfo.getSecureKeysDir()),
createSSHKeyPair(taskInfo));
context = createContext(cConf, programOptions, programRunId, taskInfo.getUser(),
- taskInfo.getProvisionerProperties(), sshContext);
+ taskInfo.getProvisionerProperties(), sshContext, errorCategory);
} catch (IOException e) {
+ String errorReason = "Failed to load ssh key.";
+ String errorMessage =
+ String.format("Failed to load ssh key with message: %s", e.getMessage());
+ Exception ex = ErrorUtils.getProgramFailureException(errorCategory, errorReason,
+ errorMessage, ErrorType.SYSTEM, false, e);
runWithProgramLogging(taskInfo.getProgramRunId(), systemArgs,
- () -> LOG.error("Failed to load ssh key. The run will be marked as failed.", e));
- programStateWriter.error(programRunId,
- new IllegalStateException("Failed to load ssh key.", e));
+ () -> LOG.error("The run will be marked as failed.", ex));
+ programStateWriter.error(programRunId, ex);
provisionerNotifier.deprovisioning(taskInfo.getProgramRunId());
return () -> {
};
} catch (InvalidMacroException e) {
- runWithProgramLogging(taskInfo.getProgramRunId(), systemArgs,
- () -> LOG.error("Could not evaluate macros while provisoning. "
- + "The run will be marked as failed.", e));
- programStateWriter.error(programRunId,
- new IllegalStateException("Could not evaluate macros while provisioning", e));
+ String errorReason = "Could not evaluate macros while provisioning.";
+ String errorMessage = String.format("Could not evaluate macros with message: %s",
+ e.getMessage());
+ ProgramFailureException ex = ErrorUtils.getProgramFailureException(errorCategory, errorReason,
+ errorMessage, ErrorType.USER, false, null);
+ ex.addSuppressed(e);
+ runWithProgramLogging(programRunId, systemArgs,
+ () -> LOG.error("The run will be marked as failed.", ex));
+ programStateWriter.error(programRunId, ex);
provisionerNotifier.deprovisioning(taskInfo.getProgramRunId());
return () -> {
};
@@ -694,7 +712,7 @@ private Runnable createProvisionTask(ProvisioningTaskInfo taskInfo, Provisioner
// TODO: (CDAP-13246) pick up timeout from profile instead of hardcoding
ProvisioningTask task = new ProvisionTask(taskInfo, transactionRunner, provisioner, context,
- provisionerNotifier, programStateWriter, 300);
+ provisionerNotifier, programStateWriter, provisionerStore, 300);
ProvisioningTaskKey taskKey = new ProvisioningTaskKey(programRunId,
ProvisioningOp.Type.PROVISION);
@@ -715,13 +733,20 @@ private Runnable createProvisionTask(ProvisioningTaskInfo taskInfo, Provisioner
private Runnable createDeprovisionTask(ProvisioningTaskInfo taskInfo, Provisioner provisioner,
Consumer taskCleanup) {
Map properties = taskInfo.getProvisionerProperties();
+ ErrorCategory errorCategory = new ErrorCategory(ErrorCategoryEnum.DEPROVISIONING);
ProvisionerContext context;
SSHKeyPair sshKeyPair = null;
try {
sshKeyPair = createSSHKeyPair(taskInfo);
} catch (IOException e) {
- LOG.warn("Failed to load ssh key. No SSH key will be available for the deprovision task", e);
+ String errorReason = "Failed to load ssh key.";
+ String errorMessage =
+ String.format("Failed to load ssh key with message: %s", e.getMessage());
+ Exception ex = ErrorUtils.getProgramFailureException(errorCategory, errorReason,
+ errorMessage, ErrorType.SYSTEM, false, e);
+ LOG.warn("Failed to load ssh key. No SSH key will be available for the deprovision task",
+ ex);
}
ProgramRunId programRunId = taskInfo.getProgramRunId();
@@ -732,18 +757,22 @@ private Runnable createDeprovisionTask(ProvisioningTaskInfo taskInfo, Provisione
Networks.getAddress(cConf, Constants.NETWORK_PROXY_ADDRESS),
null, sshKeyPair);
context = createContext(cConf, taskInfo.getProgramOptions(), programRunId, taskInfo.getUser(),
- properties,
- sshContext);
+ properties, sshContext, errorCategory);
} catch (InvalidMacroException e) {
+ String errorReason = "Could not evaluate macros while deprovisioning.";
+ String errorMessage = String.format("Could not evaluate macros with message: %s",
+ e.getMessage());
+ ProgramFailureException ex = ErrorUtils.getProgramFailureException(errorCategory, errorReason,
+ errorMessage, ErrorType.USER, false, null);
+ ex.addSuppressed(e);
runWithProgramLogging(programRunId, systemArgs,
- () -> LOG.error("Could not evaluate macros while deprovisoning. "
- + "The cluster will be marked as orphaned.", e));
+ () -> LOG.error("The cluster will be marked as orphaned.", ex));
provisionerNotifier.orphaned(programRunId);
return () -> {
};
}
DeprovisionTask task = new DeprovisionTask(taskInfo, transactionRunner, 300,
- provisioner, context, provisionerNotifier, locationFactory);
+ provisioner, provisionerStore, context, provisionerNotifier, locationFactory);
ProvisioningTaskKey taskKey = new ProvisioningTaskKey(programRunId,
ProvisioningOp.Type.DEPROVISION);
@@ -850,9 +879,8 @@ private Location createKeysDirectory(ProgramRunId programRunId) {
}
private ProvisionerContext createContext(CConfiguration cConf, ProgramOptions programOptions,
- ProgramRunId programRunId, String userId,
- Map properties,
- @Nullable SSHContext sshContext) {
+ ProgramRunId programRunId, String userId, Map properties,
+ @Nullable SSHContext sshContext, ErrorCategory errorCategory) {
RuntimeMonitorType runtimeMonitorType = SystemArguments.getRuntimeMonitorType(cConf,
programOptions);
Map systemArgs = programOptions.getArguments().asMap();
@@ -866,9 +894,8 @@ private ProvisionerContext createContext(CConfiguration cConf, ProgramOptions pr
LoggingContext loggingContext = LoggingContextHelper.getLoggingContextWithRunId(programRunId,
systemArgs);
return new DefaultProvisionerContext(programRunId, provisionerName, evaluated, sparkCompat,
- sshContext,
- appCDAPVersion, locationFactory, runtimeMonitorType,
- metricsCollectionService, profileName, contextExecutor, loggingContext);
+ sshContext, appCDAPVersion, locationFactory, runtimeMonitorType, metricsCollectionService,
+ profileName, contextExecutor, loggingContext, errorCategory);
}
/**
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/adapters/ProgramDescriptorDeserializer.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/adapters/ProgramDescriptorDeserializer.java
new file mode 100644
index 000000000000..3681aa4adde4
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/adapters/ProgramDescriptorDeserializer.java
@@ -0,0 +1,75 @@
+/*
+ * 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.provision.adapters;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import io.cdap.cdap.api.app.ApplicationSpecification;
+import io.cdap.cdap.api.artifact.ArtifactId;
+import io.cdap.cdap.app.program.ProgramDescriptor;
+import io.cdap.cdap.internal.app.store.adapters.AppSpecDeserializationContext;
+import io.cdap.cdap.internal.app.store.adapters.AppSpecDeserializationContextHolder;
+import io.cdap.cdap.proto.id.ProgramId;
+import java.lang.reflect.Type;
+
+/**
+ * Custom Gson deserializer for {@link ProgramDescriptor}. This deserializer is responsible for
+ * reconstructing a {@link ProgramDescriptor} from its JSON representation, particularly by setting
+ * the root artifact in the {@link AppSpecDeserializationContext} to enable correct plugin
+ * resolution during the deserialization of the nested {@link ApplicationSpecification}.
+ */
+public class ProgramDescriptorDeserializer implements JsonDeserializer {
+
+ @Override
+ public ProgramDescriptor deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+ AppSpecDeserializationContext appSpecDeserializationContext = AppSpecDeserializationContextHolder.getContext();
+ JsonObject jsonObject = json.getAsJsonObject();
+
+ JsonElement programIdElement = jsonObject.get("programId");
+ if (programIdElement == null || programIdElement.isJsonNull()) {
+ throw new JsonParseException("ApplicationMeta 'id' field is missing or null");
+ }
+ ProgramId programId = context.deserialize(programIdElement, ProgramId.class);
+ appSpecDeserializationContext.setNamespace(programId.getNamespaceId().getNamespace());
+
+ JsonObject specJson = jsonObject.getAsJsonObject("appSpec");
+ if (specJson == null || specJson.isJsonNull()) {
+ throw new JsonParseException(
+ "ApplicationMeta 'spec' field is missing or null for id: " + programId.getApplication());
+ }
+ // Set parent context before fully deserializing the AppSpec,
+ // so plugins can be retrieved from DB using this parent information.
+ JsonElement artifactIdJson = specJson.get("artifactId");
+ if (artifactIdJson != null && !artifactIdJson.isJsonNull()) {
+ ArtifactId artifactId = context.deserialize(artifactIdJson, ArtifactId.class);
+ if (artifactId != null) {
+ appSpecDeserializationContext.setRootArtifact(artifactId);
+ } else {
+ throw new JsonParseException("ArtifactId in ApplicationSpecification was null for app: "
+ + programId.getApplication());
+ }
+ }
+ JsonElement appNameJson = specJson.get("name");
+ appSpecDeserializationContext.setAppName(appNameJson != null ? appNameJson.getAsString() : null);
+ ApplicationSpecification spec = context.deserialize(specJson, ApplicationSpecification.class);
+ return new ProgramDescriptor(programId, spec);
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/adapters/ProvisioningTaskInfoAdapter.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/adapters/ProvisioningTaskInfoAdapter.java
new file mode 100644
index 000000000000..da4a7f4bf002
--- /dev/null
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/adapters/ProvisioningTaskInfoAdapter.java
@@ -0,0 +1,140 @@
+/*
+ * 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.provision.adapters;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import io.cdap.cdap.api.app.ApplicationSpecification;
+import io.cdap.cdap.api.plugin.Plugin;
+import io.cdap.cdap.api.plugin.PluginClass;
+import io.cdap.cdap.app.program.ProgramDescriptor;
+import io.cdap.cdap.app.runtime.Arguments;
+import io.cdap.cdap.app.runtime.ProgramOptions;
+import io.cdap.cdap.common.conf.Constants.AppMetaStore;
+import io.cdap.cdap.internal.app.ApplicationSpecificationAdapter;
+import io.cdap.cdap.internal.app.ApplicationSpecificationCodec;
+import io.cdap.cdap.internal.app.runtime.codec.ArgumentsCodec;
+import io.cdap.cdap.internal.app.runtime.codec.ProgramOptionsCodec;
+import io.cdap.cdap.internal.app.store.adapters.AppSpecDeserializationContext;
+import io.cdap.cdap.internal.app.store.adapters.AppSpecDeserializationContextHolder;
+import io.cdap.cdap.internal.app.store.adapters.PluginClassSerializer;
+import io.cdap.cdap.internal.app.store.adapters.PluginDeserializer;
+import io.cdap.cdap.internal.provision.ProvisioningTaskInfo;
+import io.cdap.cdap.spi.data.StructuredTable;
+import io.cdap.cdap.spi.data.StructuredTableContext;
+import io.cdap.cdap.spi.data.TableNotFoundException;
+import io.cdap.cdap.store.StoreDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages Gson instances specifically configured for {@link ProvisioningTaskInfo} serialization and
+ * deserialization, handling application specification reduction. This ensures thread-safe
+ * operations by managing the {@link AppSpecDeserializationContext} lifecycle per operation via
+ * {@link AppSpecDeserializationContextHolder}. It uses a static cache for Gson instances based on
+ * the {@code appSpecReductionEnabled} flag.
+ */
+public final class ProvisioningTaskInfoAdapter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ProvisioningTaskInfo.class);
+ private static final Gson GSON_INSTANCE_REDUCTION_ENABLED = buildGsonInternal(true);
+ private static final Gson GSON_INSTANCE_REDUCTION_DISABLED = buildGsonInternal(false);
+
+ private ProvisioningTaskInfoAdapter() {
+ }
+
+ private static StructuredTable getPluginDataTable(StructuredTableContext context) {
+ try {
+ return context.getTable(StoreDefinition.ArtifactStore.PLUGIN_DATA_TABLE);
+ } catch (TableNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static StructuredTable getUniversalPluginDataTable(StructuredTableContext context) {
+ try {
+ return context.getTable(StoreDefinition.ArtifactStore.UNIV_PLUGIN_DATA_TABLE);
+ } catch (TableNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isAppSpecReductionEnabled(StructuredTableContext context) {
+ return AppMetaStore.APPSPEC_REDUCTION_SUPPORTED_STORAGE_PROVIDERS.contains(
+ context.getStorageProvider());
+ }
+
+ /**
+ * Deserializes a JSON string to an object of the specified class, managing the
+ * {@link AppSpecDeserializationContext} lifecycle via
+ * {@link AppSpecDeserializationContextHolder}.
+ */
+ public static ProvisioningTaskInfo fromJson(String jsonString, String namespace,
+ StructuredTableContext context) {
+ boolean reductionEnabled = isAppSpecReductionEnabled(context);
+ AppSpecDeserializationContext opContext =
+ reductionEnabled ? new AppSpecDeserializationContext(namespace, getPluginDataTable(context),
+ getUniversalPluginDataTable(context)) : null;
+ if (opContext != null) {
+ AppSpecDeserializationContextHolder.setContext(opContext);
+ }
+ try {
+ Gson gson = getGson(reductionEnabled);
+ ProvisioningTaskInfo taskInfo = gson.fromJson(jsonString, ProvisioningTaskInfo.class);
+ if (opContext != null && !opContext.getMissingPlugins().isEmpty()) {
+ LOG.trace("Missing plugins for application {}: {}", opContext.getAppName(),
+ opContext.getMissingPlugins());
+ }
+ return taskInfo;
+ } finally {
+ AppSpecDeserializationContextHolder.clearContext();
+ }
+ }
+
+ /**
+ * Serializes an object to its JSON representation.
+ */
+ public static String toJson(Object objectToSerialize, StructuredTableContext context) {
+ Gson gson = getGson(isAppSpecReductionEnabled(context));
+ // No need to set / get the AppSpecDeserialization context in case of serialization.
+ return gson.toJson(objectToSerialize, ProvisioningTaskInfo.class);
+ }
+
+ private static Gson buildGsonInternal(boolean appSpecReductionEnabled) {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ ApplicationSpecificationAdapter.addTypeAdapters(gsonBuilder);
+ gsonBuilder.registerTypeAdapter(ProgramOptions.class, new ProgramOptionsCodec());
+ gsonBuilder.registerTypeAdapter(Arguments.class, new ArgumentsCodec());
+
+ if (appSpecReductionEnabled) {
+ gsonBuilder.registerTypeAdapter(ProgramDescriptor.class, new ProgramDescriptorDeserializer());
+ gsonBuilder.registerTypeAdapter(PluginClass.class, new PluginClassSerializer());
+ gsonBuilder.registerTypeAdapter(Plugin.class, new PluginDeserializer());
+ gsonBuilder.registerTypeHierarchyAdapter(ApplicationSpecification.class,
+ new ApplicationSpecificationCodec());
+ }
+
+ return gsonBuilder.create();
+ }
+
+ private static Gson getGson(boolean appSpecReductionEnabled) {
+ if (appSpecReductionEnabled) {
+ return GSON_INSTANCE_REDUCTION_ENABLED;
+ }
+ return GSON_INSTANCE_REDUCTION_DISABLED;
+ }
+}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/DeprovisionTask.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/DeprovisionTask.java
index 50520064be85..c63ec1a02c55 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/DeprovisionTask.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/DeprovisionTask.java
@@ -19,6 +19,7 @@
import io.cdap.cdap.common.io.Locations;
import io.cdap.cdap.internal.provision.ProvisionerNotifier;
+import io.cdap.cdap.internal.provision.ProvisionerStore;
import io.cdap.cdap.internal.provision.ProvisioningOp;
import io.cdap.cdap.internal.provision.ProvisioningTaskInfo;
import io.cdap.cdap.runtime.spi.provisioner.ClusterStatus;
@@ -59,10 +60,11 @@ public class DeprovisionTask extends ProvisioningTask {
private final Location keysDir;
public DeprovisionTask(ProvisioningTaskInfo initialTaskInfo, TransactionRunner transactionRunner,
- int retryTimeLimitSecs, Provisioner provisioner,
+ int retryTimeLimitSecs, Provisioner provisioner, ProvisionerStore provisionerStore,
ProvisionerContext provisionerContext, ProvisionerNotifier provisionerNotifier,
LocationFactory locationFactory) {
- super(provisioner, provisionerContext, initialTaskInfo, transactionRunner, retryTimeLimitSecs);
+ super(provisioner, provisionerContext, initialTaskInfo, transactionRunner, provisionerStore,
+ retryTimeLimitSecs);
this.provisionerNotifier = provisionerNotifier;
this.keysDir = locationFactory.create(initialTaskInfo.getSecureKeysDir());
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisionTask.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisionTask.java
index 9b545edb736a..e0172b877236 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisionTask.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisionTask.java
@@ -23,6 +23,7 @@
import io.cdap.cdap.common.service.Retries;
import io.cdap.cdap.common.service.RetryStrategies;
import io.cdap.cdap.internal.provision.ProvisionerNotifier;
+import io.cdap.cdap.internal.provision.ProvisionerStore;
import io.cdap.cdap.internal.provision.ProvisioningOp;
import io.cdap.cdap.internal.provision.ProvisioningTaskInfo;
import io.cdap.cdap.runtime.spi.provisioner.ClusterStatus;
@@ -77,8 +78,9 @@ public class ProvisionTask extends ProvisioningTask {
public ProvisionTask(ProvisioningTaskInfo initialTaskInfo, TransactionRunner transactionRunner,
Provisioner provisioner, ProvisionerContext provisionerContext,
ProvisionerNotifier provisionerNotifier, ProgramStateWriter programStateWriter,
- int retryTimeLimitSecs) {
- super(provisioner, provisionerContext, initialTaskInfo, transactionRunner, retryTimeLimitSecs);
+ ProvisionerStore provisionerStore, int retryTimeLimitSecs) {
+ super(provisioner, provisionerContext, initialTaskInfo, transactionRunner, provisionerStore,
+ retryTimeLimitSecs);
this.provisionerNotifier = provisionerNotifier;
this.programStateWriter = programStateWriter;
}
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisioningTask.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisioningTask.java
index 4cd655642c12..2ada9d9b53a5 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisioningTask.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/internal/provision/task/ProvisioningTask.java
@@ -17,6 +17,10 @@
package io.cdap.cdap.internal.provision.task;
+import io.cdap.cdap.api.exception.ErrorType;
+import io.cdap.cdap.api.exception.ErrorUtils;
+import io.cdap.cdap.api.exception.FailureDetailsProvider;
+import io.cdap.cdap.api.exception.ProgramFailureException;
import io.cdap.cdap.common.async.RepeatedTask;
import io.cdap.cdap.common.lang.Exceptions;
import io.cdap.cdap.common.logging.LogSamplers;
@@ -24,7 +28,7 @@
import io.cdap.cdap.common.service.Retries;
import io.cdap.cdap.common.service.RetryStrategies;
import io.cdap.cdap.common.service.RetryStrategy;
-import io.cdap.cdap.internal.provision.ProvisionerTable;
+import io.cdap.cdap.internal.provision.ProvisionerStore;
import io.cdap.cdap.internal.provision.ProvisioningOp;
import io.cdap.cdap.internal.provision.ProvisioningTaskInfo;
import io.cdap.cdap.internal.provision.ProvisioningTaskKey;
@@ -62,6 +66,7 @@ public abstract class ProvisioningTask implements RepeatedTask {
private final TransactionRunner transactionRunner;
private final ProvisioningTaskKey taskKey;
private final ProvisioningTaskInfo initialTaskInfo;
+ private final ProvisionerStore provisionerStore;
private ProvisioningTaskInfo taskInfo;
private RetryStrategy retryStrategy;
@@ -72,10 +77,11 @@ public abstract class ProvisioningTask implements RepeatedTask {
protected ProvisioningTask(Provisioner provisioner, ProvisionerContext provisionerContext,
ProvisioningTaskInfo initialTaskInfo, TransactionRunner transactionRunner,
- int retryTimeLimitSecs) {
+ ProvisionerStore provisionerStore, int retryTimeLimitSecs) {
this.provisioner = provisioner;
this.provisionerContext = provisionerContext;
this.initialTaskInfo = initialTaskInfo;
+ this.provisionerStore = provisionerStore;
this.taskKey = new ProvisioningTaskKey(initialTaskInfo.getProgramRunId(),
initialTaskInfo.getProvisioningOp().getType());
this.programRunId = initialTaskInfo.getProgramRunId();
@@ -105,14 +111,16 @@ public final long executeOnce() throws Exception {
}
// Get the sub-task to execute
+ ProvisioningOp.Type type = currentTaskInfo.getProvisioningOp().getType();
ProvisioningSubtask subtask = subTasks.get(state);
if (subtask == null) {
// should never happen
- throw new IllegalStateException(
- String.format("Invalid state '%s' in provisioning task for program run '%s'. "
- + "This means there is a bug in provisioning state machine. "
- + "Please reach out to the development team.",
- state, programRunId));
+ String errorReason = String.format("Invalid state '%s' in provisioning task for "
+ + "program run '%s'.", state, programRunId);
+ String errorMessage = String.format("%s This means there is a bug in provisioning state"
+ + "machine. Please reach out to the development team.", errorReason);
+ throw ErrorUtils.getProgramFailureException(provisionerContext.getErrorCategory(),
+ errorReason, errorMessage, ErrorType.SYSTEM, false, null);
}
if (subtask == EndSubtask.INSTANCE) {
LOG.debug("Completed {} task for program run {}.",
@@ -158,10 +166,16 @@ public final long executeOnce() throws Exception {
} catch (InterruptedException e) {
throw e;
} catch (Throwable e) {
- LOG.error("{} task failed in {} state for program run {} due to {}.",
- currentTaskInfo.getProvisioningOp().getType(), state, programRunId,
- Exceptions.condenseThrowableMessage(e), e);
- handleSubtaskFailure(currentTaskInfo, e);
+ String errorReason = String.format("'%s' task failed in '%s' state for program run '%s'",
+ currentTaskInfo.getProvisioningOp().getType(), state, programRunId);
+ ProgramFailureException ex = null;
+ if (!(e instanceof FailureDetailsProvider)) {
+ ex = ErrorUtils.getProgramFailureException(provisionerContext.getErrorCategory(),
+ errorReason, Exceptions.condenseThrowableMessage(e), ErrorType.UNKNOWN, false, e);
+ }
+ LOG.error("{} due to {}.", errorReason,
+ Exceptions.condenseThrowableMessage(ex == null ? e : ex), ex == null ? e : ex);
+ handleSubtaskFailure(currentTaskInfo, ex);
ProvisioningOp failureOp = new ProvisioningOp(currentTaskInfo.getProvisioningOp().getType(),
ProvisioningOp.Status.FAILED);
ProvisioningTaskInfo failureInfo = new ProvisioningTaskInfo(currentTaskInfo, failureOp,
@@ -174,7 +188,7 @@ public final long executeOnce() throws Exception {
}
/**
- * Write the task state to the {@link ProvisionerTable}, retrying if any exception is caught.
+ * Write the task state to the {@link ProvisionerStore}, retrying if any exception is caught.
* Before persisting the state, the current state will be checked. If the current state is
* cancelled, it will not be overwritten.
*
@@ -192,14 +206,13 @@ private ProvisioningTaskInfo persistTaskInfo(ProvisioningTaskInfo taskInfo,
// Stop retrying if we are interrupted. Otherwise, retry on every exception, up to the retry limit
return Retries.callWithInterruptibleRetries(
() -> TransactionRunners.run(transactionRunner, context -> {
- ProvisionerTable provisionerTable = new ProvisionerTable(context);
- ProvisioningTaskInfo currentState = provisionerTable.getTaskInfo(taskKey);
+ ProvisioningTaskInfo currentState = provisionerStore.getTaskInfo(taskKey);
// if the state is cancelled, don't write anything and transition to the end subtask.
if (currentState != null && currentState.getProvisioningOp().getStatus()
== ProvisioningOp.Status.CANCELLED) {
return currentState;
}
- provisionerTable.putTaskInfo(taskInfo);
+ provisionerStore.putTaskInfo(taskInfo);
return taskInfo;
}), retryStrategy, t -> true);
} catch (RuntimeException e) {
diff --git a/cdap-app-fabric/src/main/java/io/cdap/cdap/scheduler/CoreSchedulerService.java b/cdap-app-fabric/src/main/java/io/cdap/cdap/scheduler/CoreSchedulerService.java
index d60b815c03ea..51507d596861 100644
--- a/cdap-app-fabric/src/main/java/io/cdap/cdap/scheduler/CoreSchedulerService.java
+++ b/cdap-app-fabric/src/main/java/io/cdap/cdap/scheduler/CoreSchedulerService.java
@@ -25,6 +25,7 @@
import com.google.inject.Inject;
import io.cdap.cdap.api.dataset.lib.CloseableIterator;
import io.cdap.cdap.api.security.AccessException;
+import io.cdap.cdap.api.service.ServiceUnavailableException;
import io.cdap.cdap.app.program.ProgramDescriptor;
import io.cdap.cdap.app.store.Store;
import io.cdap.cdap.common.AlreadyExistsException;
@@ -32,7 +33,6 @@
import io.cdap.cdap.common.ConflictException;
import io.cdap.cdap.common.NotFoundException;
import io.cdap.cdap.common.ProfileConflictException;
-import io.cdap.cdap.api.service.ServiceUnavailableException;
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.service.RetryOnStartFailureService;
import io.cdap.cdap.internal.app.runtime.ProgramOptionConstants;
@@ -48,8 +48,8 @@
import io.cdap.cdap.internal.app.runtime.schedule.store.Schedulers;
import io.cdap.cdap.internal.app.store.profile.ProfileStore;
import io.cdap.cdap.internal.profile.AdminEventPublisher;
-import io.cdap.cdap.messaging.spi.MessagingService;
import io.cdap.cdap.messaging.context.MultiThreadMessagingContext;
+import io.cdap.cdap.messaging.spi.MessagingService;
import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.id.ApplicationId;
import io.cdap.cdap.proto.id.NamespaceId;
@@ -98,6 +98,7 @@ public class CoreSchedulerService extends AbstractIdleService implements Schedul
@Inject
CoreSchedulerService(TimeSchedulerService timeSchedulerService,
+ ScheduleNotificationSubscriberService scheduleNotificationSubscriberService,
ConstraintCheckerService constraintCheckerService,
MessagingService messagingService,
CConfiguration cConf, Store store, Impersonator impersonator,
@@ -123,12 +124,14 @@ protected void startUp() {
timeSchedulerService.startAndWait();
cleanupJobs();
constraintCheckerService.startAndWait();
+ scheduleNotificationSubscriberService.startAndWait();
startedLatch.countDown();
LOG.info("Started core scheduler service.");
}
@Override
protected void shutDown() {
+ scheduleNotificationSubscriberService.stopAndWait();
constraintCheckerService.stopAndWait();
timeSchedulerService.stopAndWait();
LOG.info("Stopped core scheduler service.");
@@ -567,8 +570,7 @@ public void reEnableSchedules(NamespaceId namespaceId, long startTimeMillis, lon
}
/**
- * Gets a copy of the given {@link ProgramSchedule} and add user and artifact ID in the schedule
- * properties
+ * Gets a copy of the given {@link ProgramSchedule} and add user and artifact ID in the schedule properties.
* TODO CDAP-13662 - move logic to find artifactId and userId to dashboard service and remove this method
*/
private ProgramSchedule getProgramScheduleWithUserAndArtifactId(ProgramSchedule schedule) {
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..f9e02e62d815 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,8 @@
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.ProgramScheduleHttpHandler;
import io.cdap.cdap.gateway.handlers.WorkflowHttpHandler;
import io.cdap.cdap.gateway.handlers.util.AbstractAppFabricHttpHandler;
import io.cdap.cdap.internal.app.BufferFileInputStream;
@@ -106,6 +108,8 @@ public class AppFabricClient {
private final LocationFactory locationFactory;
private final AppLifecycleHttpHandler appLifecycleHttpHandler;
private final ProgramLifecycleHttpHandler programLifecycleHttpHandler;
+ private final ProgramRuntimeHttpHandler programRuntimeHttpHandler;
+ private final ProgramScheduleHttpHandler programScheduleHttpHandler;
private final WorkflowHttpHandler workflowHttpHandler;
private final NamespaceHttpHandler namespaceHttpHandler;
private final NamespaceQueryAdmin namespaceQueryAdmin;
@@ -114,12 +118,16 @@ public class AppFabricClient {
public AppFabricClient(LocationFactory locationFactory,
AppLifecycleHttpHandler appLifecycleHttpHandler,
ProgramLifecycleHttpHandler programLifecycleHttpHandler,
+ ProgramRuntimeHttpHandler programRuntimeHttpHandler,
+ ProgramScheduleHttpHandler programScheduleHttpHandler,
NamespaceHttpHandler namespaceHttpHandler,
NamespaceQueryAdmin namespaceQueryAdmin,
WorkflowHttpHandler workflowHttpHandler) {
this.locationFactory = locationFactory;
this.appLifecycleHttpHandler = appLifecycleHttpHandler;
this.programLifecycleHttpHandler = programLifecycleHttpHandler;
+ this.programRuntimeHttpHandler = programRuntimeHttpHandler;
+ this.programScheduleHttpHandler = programScheduleHttpHandler;
this.namespaceHttpHandler = namespaceHttpHandler;
this.namespaceQueryAdmin = namespaceQueryAdmin;
this.workflowHttpHandler = workflowHttpHandler;
@@ -241,7 +249,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 +257,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 +272,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 +282,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);
}
@@ -286,7 +294,7 @@ public List getProgramSchedules(String namespace, String app, St
getNamespacePath(namespace), app, workflow);
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri);
try {
- programLifecycleHttpHandler.getProgramSchedules(request, responder, namespace, app, workflow, null, null, null);
+ programScheduleHttpHandler.getProgramSchedules(request, responder, namespace, app, workflow, null, null, null);
} catch (Exception e) {
// cannot happen
throw Throwables.propagate(e);
@@ -389,8 +397,7 @@ public void suspend(String namespaceId, String appId, String scheduleName) throw
String uri = String.format("%s/apps/%s/schedules/%s/suspend", getNamespacePath(namespaceId), appId, scheduleName);
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri);
HttpUtil.setContentLength(request, request.content().readableBytes());
- programLifecycleHttpHandler.performAction(request, responder, namespaceId, appId,
- "schedules", scheduleName, "suspend");
+ programScheduleHttpHandler.performAction(request, responder, namespaceId, appId, scheduleName, "suspend");
verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Suspend workflow schedules failed");
}
@@ -399,8 +406,7 @@ public void resume(String namespaceId, String appId, String schedName) throws Ex
String uri = String.format("%s/apps/%s/schedules/%s/resume", getNamespacePath(namespaceId), appId, schedName);
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri);
HttpUtil.setContentLength(request, request.content().readableBytes());
- programLifecycleHttpHandler.performAction(request, responder, namespaceId, appId,
- "schedules", schedName, "resume");
+ programScheduleHttpHandler.performAction(request, responder, namespaceId, appId, schedName, "resume");
verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Resume workflow schedules failed");
}
@@ -410,7 +416,7 @@ public String scheduleStatus(String namespaceId, String appId, String schedId, i
String uri = String.format("%s/apps/%s/schedules/%s/status", getNamespacePath(namespaceId), appId, schedId);
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri);
try {
- programLifecycleHttpHandler.getStatus(request, responder, namespaceId, appId, "schedules", schedId);
+ programScheduleHttpHandler.getStatus(request, responder, namespaceId, appId, schedId);
} catch (NotFoundException e) {
return "NOT_FOUND";
}
@@ -614,7 +620,7 @@ public void addSchedule(ApplicationId application, ScheduleDetail scheduleDetail
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, uri);
request.content().writeCharSequence(GSON.toJson(scheduleDetail), StandardCharsets.UTF_8);
HttpUtil.setContentLength(request, request.content().readableBytes());
- programLifecycleHttpHandler.addScheduleVersioned(request, responder, application.getNamespace(),
+ programScheduleHttpHandler.addScheduleVersioned(request, responder, application.getNamespace(),
application.getApplication(), application.getVersion(),
scheduleDetail.getName());
verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Add schedule failed");
@@ -627,9 +633,8 @@ public void enableSchedule(ScheduleId scheduleId) throws Exception {
scheduleId.getSchedule());
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri);
HttpUtil.setContentLength(request, 0);
- programLifecycleHttpHandler.performAction(request, responder, scheduleId.getNamespace(),
- scheduleId.getApplication(), "schedules",
- scheduleId.getSchedule(), "enable");
+ programScheduleHttpHandler.performAction(request, responder, scheduleId.getNamespace(),
+ scheduleId.getApplication(), scheduleId.getSchedule(), "enable");
verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Enable schedule failed");
}
@@ -642,7 +647,7 @@ public void updateSchedule(ScheduleId scheduleId, ScheduleDetail scheduleDetail)
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri);
request.content().writeCharSequence(GSON.toJson(scheduleDetail), StandardCharsets.UTF_8);
HttpUtil.setContentLength(request, request.content().readableBytes());
- programLifecycleHttpHandler.updateScheduleVersioned(request, responder, application.getNamespace(),
+ programScheduleHttpHandler.updateScheduleVersioned(request, responder, application.getNamespace(),
application.getApplication(), application.getVersion(),
scheduleId.getSchedule());
verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Update schedule failed");
@@ -654,7 +659,7 @@ public void deleteSchedule(ScheduleId scheduleId) throws Exception {
String uri = String.format("%s/apps/%s/versions/%s/schedules/%s", getNamespacePath(application.getNamespace()),
application.getApplication(), application.getVersion(), scheduleId.getSchedule());
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.DELETE, uri);
- programLifecycleHttpHandler.deleteScheduleVersioned(request, responder, application.getNamespace(),
+ programScheduleHttpHandler.deleteScheduleVersioned(request, responder, application.getNamespace(),
application.getApplication(), application.getVersion(),
scheduleId.getSchedule());
verifyResponse(HttpResponseStatus.OK, responder.getStatus(), "Delete schedule failed");
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRequestQueueTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRequestQueueTest.java
index da9641eef21a..9821561394eb 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRequestQueueTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/DefaultPreviewRequestQueueTest.java
@@ -142,6 +142,12 @@ public byte[] getPreviewRequestPollerInfo(ApplicationId applicationId) {
return new byte[0];
}
+ @Nullable
+ @Override
+ public ApplicationId getApplicationId(byte[] pollerInfo) {
+ return null;
+ }
+
@Override
public void deleteExpiredData(long ttlInSeconds) {
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnableTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnableTest.java
index dbb29302229d..554c6cc538ca 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnableTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/preview/PreviewRunnerTwillRunnableTest.java
@@ -36,7 +36,7 @@ public void testNoSQLInjector() {
CConfiguration cConf = CConfiguration.create();
cConf.set(Constants.Dataset.DATA_STORAGE_IMPLEMENTATION, Constants.Dataset.DATA_STORAGE_NOSQL);
Injector injector = PreviewRunnerTwillRunnable.createInjector(cConf, new Configuration(),
- new PreviewRequestPollerInfo(0, "testuid"));
+ new PreviewRequestPollerInfo(0, "testuid", null));
DefaultPreviewRunnerManager defaultPreviewRunnerManager = (DefaultPreviewRunnerManager) injector
.getInstance(PreviewRunnerManager.class);
Injector previewInjector = defaultPreviewRunnerManager.createPreviewInjector();
@@ -48,7 +48,7 @@ public void testPostgresQLInjector() {
CConfiguration cConf = CConfiguration.create();
cConf.set(Constants.Dataset.DATA_STORAGE_IMPLEMENTATION, Constants.Dataset.DATA_STORAGE_SQL);
Injector injector = PreviewRunnerTwillRunnable.createInjector(cConf, new Configuration(),
- new PreviewRequestPollerInfo(0, "testuid"));
+ new PreviewRequestPollerInfo(0, "testuid", null));
DefaultPreviewRunnerManager defaultPreviewRunnerManager = (DefaultPreviewRunnerManager) injector
.getInstance(PreviewRunnerManager.class);
Injector previewInjector = defaultPreviewRunnerManager.createPreviewInjector();
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisherTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisherTest.java
index 9ffa8a7a77eb..a6c1e25b98bf 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisherTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/program/MessagingProgramStatePublisherTest.java
@@ -29,6 +29,7 @@
import io.cdap.cdap.proto.id.ApplicationId;
import io.cdap.cdap.proto.id.ProgramRunId;
import java.io.IOException;
+import java.net.SocketTimeoutException;
import java.util.Map;
import org.junit.Assert;
import org.junit.Rule;
@@ -95,4 +96,65 @@ public void testMultipleTopics_noRun() throws TopicNotFoundException, IOExceptio
StoreRequest storeRequest = storeRequestCaptor.getValue();
Assert.assertEquals("programstatusevent0", storeRequest.getTopicId().getTopic());
}
+
+ @Test
+ public void testPublishSuccessOnRetryableException() throws TopicNotFoundException, IOException {
+ CConfiguration cConf = CConfiguration.create();
+ cConf.setInt(Constants.AppFabric.PROGRAM_STATUS_EVENT_NUM_PARTITIONS, 1);
+ cConf.setInt("system.program.state.retry.policy.max.retries", 3);
+
+ MessagingService messagingService = Mockito.mock(MessagingService.class);
+ MessagingProgramStatePublisher publisher = new MessagingProgramStatePublisher(cConf, messagingService);
+ Mockito.when(messagingService.publish(Mockito.any()))
+ .thenThrow(new SocketTimeoutException())
+ .thenReturn(null);
+
+ publisher.publish(Notification.Type.PROGRAM_STATUS, ImmutableMap.of());
+
+ Mockito.verify(messagingService, Mockito.times(2)).publish(storeRequestCaptor.capture());
+ StoreRequest storeRequest = storeRequestCaptor.getValue();
+ Assert.assertEquals("programstatusevent", storeRequest.getTopicId().getTopic());
+ }
+
+ @Test
+ public void testPublishThrowsOnRetryExhausted() throws TopicNotFoundException, IOException {
+ CConfiguration cConf = CConfiguration.create();
+ cConf.setInt(Constants.AppFabric.PROGRAM_STATUS_EVENT_NUM_PARTITIONS, 1);
+ cConf.setInt("system.program.state.retry.policy.max.retries", 3);
+
+ MessagingService messagingService = Mockito.mock(MessagingService.class);
+ MessagingProgramStatePublisher publisher = new MessagingProgramStatePublisher(cConf, messagingService);
+ Mockito.when(messagingService.publish(Mockito.any()))
+ .thenThrow(new SocketTimeoutException());
+
+ RuntimeException outerException = Assert.assertThrows(RuntimeException.class,
+ () -> publisher.publish(Notification.Type.PROGRAM_STATUS, ImmutableMap.of()));
+ Assert.assertNotNull(outerException.getCause());
+ Assert.assertTrue(outerException.getCause() instanceof SocketTimeoutException);
+
+ Mockito.verify(messagingService, Mockito.times(4)).publish(storeRequestCaptor.capture());
+ StoreRequest storeRequest = storeRequestCaptor.getValue();
+ Assert.assertEquals("programstatusevent", storeRequest.getTopicId().getTopic());
+ }
+
+ @Test
+ public void testPublishThrowsForNonRetryableException() throws TopicNotFoundException, IOException {
+ CConfiguration cConf = CConfiguration.create();
+ cConf.setInt(Constants.AppFabric.PROGRAM_STATUS_EVENT_NUM_PARTITIONS, 1);
+
+ MessagingService messagingService = Mockito.mock(MessagingService.class);
+ MessagingProgramStatePublisher publisher = new MessagingProgramStatePublisher(cConf,
+ messagingService);
+ Mockito.when(messagingService.publish(Mockito.any()))
+ .thenThrow(new IOException())
+ .thenReturn(null);
+
+ RuntimeException outerException = Assert.assertThrows(RuntimeException.class,
+ () -> publisher.publish(Notification.Type.PROGRAM_STATUS, ImmutableMap.of()));
+ Assert.assertNotNull(outerException.getCause());
+ Assert.assertTrue(outerException.getCause() instanceof IOException);
+ Mockito.verify(messagingService).publish(storeRequestCaptor.capture());
+ StoreRequest storeRequest = storeRequestCaptor.getValue();
+ Assert.assertEquals("programstatusevent", storeRequest.getTopicId().getTopic());
+ }
}
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleServiceTest.java
index c411940e7230..d008739fe573 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleServiceTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ApplicationLifecycleServiceTest.java
@@ -34,6 +34,7 @@
import io.cdap.cdap.common.ArtifactNotFoundException;
import io.cdap.cdap.common.BadRequestException;
import io.cdap.cdap.common.conf.Constants;
+import io.cdap.cdap.common.conf.Constants.Gateway;
import io.cdap.cdap.common.id.Id;
import io.cdap.cdap.common.id.Id.Namespace;
import io.cdap.cdap.common.io.Locations;
@@ -843,6 +844,24 @@ public void testUpdateSourceControlMetaWithEmptyGitFileHash() throws Exception {
);
}
+ @Test
+ public void testGetApplicationCount() throws Exception {
+ createNamespace("customNS");
+ deploy(AllProgramsApp.class, 200, Constants.Gateway.API_VERSION_3_TOKEN, "customNS");
+ deploy(ConfigTestApp.class, 200, Gateway.API_VERSION_3_TOKEN, "customNS");
+
+ long count = applicationLifecycleService.getApplicationsCount(new NamespaceId("customNS"));
+
+ Assert.assertEquals(2, count);
+ deleteNamespace("customNS");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testGetApplicationCountNullNamespace() {
+ applicationLifecycleService.getApplicationsCount(null);
+ }
+
+
private void waitForRuns(int expected, final ProgramId programId, final ProgramRunStatus status) throws Exception {
Tasks.waitFor(expected, () -> getProgramRuns(Id.Program.fromEntityId(programId), status).size(),
5, TimeUnit.SECONDS);
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/ProgramNotificationSubscriberServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberServiceTest.java
index 6c2a9cfd6f5b..51a004cf8848 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberServiceTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/ProgramNotificationSubscriberServiceTest.java
@@ -361,7 +361,7 @@ public void testLaunchingCountMetricsOnRestart() throws Exception {
// terminate the main service.
notificationService.shutDown();
notificationService.startUp();
- // Running counts are not based on metadata store in RunRecordMonitorService so not asserting it
+ // Running counts are not based on metadata store in FlowControlService so not asserting it
// here.
Tasks.waitFor(0L, () -> queryMetrics(metricStore,
SYSTEM_METRIC_PREFIX + FlowControl.LAUNCHING_COUNT, new HashMap<>()), 10, TimeUnit.SECONDS);
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/app/services/http/AppFabricTestBase.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/AppFabricTestBase.java
index ef407808268b..09843dfae79c 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/AppFabricTestBase.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/AppFabricTestBase.java
@@ -646,17 +646,13 @@ protected List getAppList(String namespace) throws Exception {
return readResponse(response, LIST_JSON_OBJECT_TYPE);
}
- protected JsonObject getAppListForPaginatedApi(String namespace, int pageSize, String token,
- String filter) throws Exception {
- return getAppListForPaginatedApi(namespace, pageSize, token, null, filter, null, null, null);
+ protected long getAppCount(String namespace) throws Exception {
+ HttpResponse response = doGet(getVersionedApiPath("apps/count",
+ Constants.Gateway.API_VERSION_3_TOKEN, namespace));
+ assertResponseCode(200, response);
+ return Long.parseLong(response.getResponseBodyAsString());
}
- protected JsonObject getAppListForPaginatedApi(String namespace, int pageSize, String token,
- String filter, String nameFilterType,
- Boolean latestOnly, Boolean sortCreationTime) throws Exception {
- return getAppListForPaginatedApi(namespace, pageSize, token, null, filter, nameFilterType, latestOnly,
- sortCreationTime);
- }
protected List getAppListForNegativePaginatedApi(String namespace, int pageSize) throws Exception {
String uri = "apps/?pageSize=" + pageSize;
@@ -667,10 +663,38 @@ protected List getAppListForNegativePaginatedApi(String namespace, i
return readResponse(response, LIST_JSON_OBJECT_TYPE);
}
- protected JsonObject getAppListForPaginatedApi(String namespace, int pageSize, String token,
+ protected List getAppListForDefaultPaginationDisabledApi(String namespace,
+ Boolean enabledDefaultPagination) throws Exception {
+ String uri = "apps/?enabledDefaultPagination=" + enabledDefaultPagination;
+
+ HttpResponse response = doGet(getVersionedApiPath(uri,
+ Constants.Gateway.API_VERSION_3_TOKEN, namespace));
+ assertResponseCode(200, response);
+ return readResponse(response, LIST_JSON_OBJECT_TYPE);
+ }
+
+ protected JsonObject getAppListForPaginatedApi(String namespace, Integer pageSize, String token,
+ String filter) throws Exception {
+ return getAppListForPaginatedApi(namespace, pageSize, token, null, filter, null, null, null, false);
+ }
+
+ protected JsonObject getAppListForPaginatedApi(String namespace, Integer pageSize, String token,
+ String filter, String nameFilterType,
+ Boolean latestOnly, Boolean sortCreationTime,
+ boolean enableDefaultPagination) throws Exception {
+ return getAppListForPaginatedApi(namespace, pageSize, token, null, filter, nameFilterType, latestOnly,
+ sortCreationTime, enableDefaultPagination);
+ }
+
+ protected JsonObject getAppListForPaginatedApi(String namespace, Integer pageSize, String token,
String orderBy, String filter, String nameFilterType,
- Boolean latestOnly, Boolean sortCreationTime) throws Exception {
- String uri = "apps/?pageSize=" + pageSize;
+ Boolean latestOnly, Boolean sortCreationTime,
+ boolean enableDefaultPagination) throws Exception {
+ String uri = "apps/?";
+
+ if (pageSize != null) {
+ uri += ("&pageSize=" + pageSize);
+ }
if (token != null) {
uri += ("&pageToken=" + token);
@@ -696,6 +720,11 @@ protected JsonObject getAppListForPaginatedApi(String namespace, int pageSize, S
uri += ("&sortCreationTime=" + sortCreationTime);
}
+ if (enableDefaultPagination) {
+ uri += ("&enableDefaultPagination=" + enableDefaultPagination);
+ }
+
+ uri = uri.replace("?&", "?");
HttpResponse response = doGet(getVersionedApiPath(uri,
Constants.Gateway.API_VERSION_3_TOKEN, namespace));
assertResponseCode(200, response);
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/AppLifecycleHttpHandlerTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/AppLifecycleHttpHandlerTest.java
index 54d01986ee59..4e69de6b8491 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/AppLifecycleHttpHandlerTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/AppLifecycleHttpHandlerTest.java
@@ -53,6 +53,7 @@
import io.cdap.cdap.internal.app.deploy.pipeline.ApplicationWithPrograms;
import io.cdap.cdap.internal.app.runtime.SystemArguments;
import io.cdap.cdap.internal.app.runtime.artifact.ArtifactRepository;
+import io.cdap.cdap.internal.app.runtime.schedule.ScheduleManager;
import io.cdap.cdap.internal.app.services.ApplicationLifecycleService;
import io.cdap.cdap.internal.app.services.http.AppFabricTestBase;
import io.cdap.cdap.internal.app.store.state.AppStateKey;
@@ -75,7 +76,6 @@
import io.cdap.cdap.proto.id.ProgramId;
import io.cdap.cdap.proto.id.ProgramReference;
import io.cdap.cdap.proto.profile.Profile;
-import io.cdap.cdap.scheduler.Scheduler;
import io.cdap.cdap.security.impersonation.CurrentUGIProvider;
import io.cdap.cdap.security.impersonation.Impersonator;
import io.cdap.cdap.security.impersonation.OwnerAdmin;
@@ -112,6 +112,7 @@ public class AppLifecycleHttpHandlerTest extends AppFabricTestBase {
@BeforeClass
public static void beforeClass() throws Throwable {
cConf = createBasicCconf();
+ cConf.set(Constants.GET_APPS_DEFAULT_PAGE_SIZE, "4");
initializeAndStartServices(cConf);
}
@@ -135,7 +136,7 @@ protected void configure() {
@Provides
@Singleton
public ApplicationLifecycleService createLifeCycleService(CConfiguration cConf,
- Store store, Scheduler scheduler, UsageRegistry usageRegistry,
+ Store store, ScheduleManager scheduleManager, UsageRegistry usageRegistry,
PreferencesService preferencesService, MetricsSystemClient metricsSystemClient,
OwnerAdmin ownerAdmin, ArtifactRepository artifactRepository,
ManagerFactory managerFactory,
@@ -144,7 +145,7 @@ public ApplicationLifecycleService createLifeCycleService(CConfiguration cConf,
MessagingService messagingService, Impersonator impersonator,
CapabilityReader capabilityReader) {
- return Mockito.spy(new ApplicationLifecycleService(cConf, store, scheduler,
+ return Mockito.spy(new ApplicationLifecycleService(cConf, store, scheduleManager,
usageRegistry, preferencesService, metricsSystemClient, ownerAdmin, artifactRepository,
managerFactory, metadataServiceClient, accessEnforcer, authenticationContext,
messagingService, impersonator, capabilityReader, new NoOpMetricsCollectionService()));
@@ -686,6 +687,79 @@ public void testListAndGetForPaginatedAPI() throws Exception {
Assert.assertEquals(0, apps.size());
}
+ @Test
+ public void testListAppsWithDefaultPaginationEnabled() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ // deploy with name to testnamespace1
+ String ns1AppName = AllProgramsApp.NAME + i;
+ Id.Namespace ns1 = Id.Namespace.from(TEST_NAMESPACE1);
+ Id.Artifact ns1ArtifactId = Id.Artifact.from(ns1, AllProgramsApp.class.getSimpleName(),
+ "1.0.0-SNAPSHOT");
+
+ HttpResponse response = addAppArtifact(ns1ArtifactId, AllProgramsApp.class);
+ Assert.assertEquals(200, response.getResponseCode());
+ Id.Application appId = Id.Application.from(ns1, ns1AppName);
+ response = deploy(appId,
+ new AppRequest<>(ArtifactSummary.from(ns1ArtifactId.toArtifactId())));
+ Assert.assertEquals(200, response.getResponseCode());
+ }
+
+ int count = 0;
+ String token = null;
+ boolean isLastPage = false;
+ boolean emptyListReceived = false;
+
+ while (!isLastPage) {
+ JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, null, token, null, null, null, null, true);
+ int currentResultSize = result.get("applications").getAsJsonArray().size();
+ count += currentResultSize;
+ emptyListReceived = (currentResultSize == 0);
+ token = result.get("nextPageToken") == null ? null : result.get("nextPageToken").getAsString();
+ isLastPage = (token == null);
+ if (isLastPage) {
+ Assert.assertEquals(2, currentResultSize);
+ }
+ else {
+ Assert.assertEquals(4, currentResultSize);
+ }
+ }
+ Assert.assertEquals(10, count);
+ Assert.assertFalse(emptyListReceived);
+
+ HttpResponse response = doDelete(getVersionedApiPath("apps/",
+ Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE1));
+ Assert.assertEquals(200, response.getResponseCode());
+ List apps = getAppList(TEST_NAMESPACE1);
+ Assert.assertEquals(0, apps.size());
+ }
+
+ @Test
+ public void testListAppsWithDefaultPaginationDisabled() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ String ns1AppName = AllProgramsApp.NAME + i;
+ Id.Namespace ns1 = Id.Namespace.from(TEST_NAMESPACE1);
+ Id.Artifact ns1ArtifactId = Id.Artifact.from(ns1, AllProgramsApp.class.getSimpleName(),
+ "1.0.0-SNAPSHOT");
+
+ HttpResponse response = addAppArtifact(ns1ArtifactId, AllProgramsApp.class);
+ Assert.assertEquals(200, response.getResponseCode());
+ Id.Application appId = Id.Application.from(ns1, ns1AppName);
+ response = deploy(appId,
+ new AppRequest<>(ArtifactSummary.from(ns1ArtifactId.toArtifactId())));
+ Assert.assertEquals(200, response.getResponseCode());
+ }
+
+ List result = getAppListForDefaultPaginationDisabledApi(TEST_NAMESPACE1, false);
+ int resultSize = result.size();
+ Assert.assertEquals(10, resultSize);
+
+ HttpResponse response = doDelete(getVersionedApiPath("apps/",
+ Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE1));
+ Assert.assertEquals(200, response.getResponseCode());
+ List apps = getAppList(TEST_NAMESPACE1);
+ Assert.assertEquals(0, apps.size());
+ }
+
@Test
public void testListAppsWithNegativePageSize() throws Exception {
for (int i = 0; i < 10; i++) {
@@ -821,17 +895,18 @@ public void testListAndGetForPaginatedAPIWithNameFilterType() throws Exception {
String token = null;
JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token,
AllProgramsApp.NAME.toUpperCase(Locale.ROOT),
- "EQUALS", null, null);
+ "EQUALS", null, null, false);
int currentResultSize = result.get("applications").getAsJsonArray().size();
Assert.assertEquals(0, currentResultSize);
JsonObject result1 = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token,
AllProgramsApp.NAME.toUpperCase(Locale.ROOT),
- "EQUALS_IGNORE_CASE", null, null);
+ "EQUALS_IGNORE_CASE", null, null, false);
int currentResultSize1 = result1.get("applications").getAsJsonArray().size();
Assert.assertEquals(1, currentResultSize1);
- JsonObject result2 = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token, AllProgramsApp.NAME, null, null, null);
+ JsonObject result2 = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token, AllProgramsApp.NAME,
+ null, null, null, false);
int currentResultSize2 = result2.get("applications").getAsJsonArray().size();
Assert.assertEquals(2, currentResultSize2);
@@ -879,7 +954,8 @@ public void testListAndGetForPaginatedAPIWithLatestOnlyLCMFlagEnabled() throws E
boolean isLastPage = false;
int currentResultSize = 0;
while (!isLastPage) {
- JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token, AllProgramsApp.NAME, null, true, null);
+ JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token,
+ AllProgramsApp.NAME, null, true, null, false);
currentResultSize = result.get("applications").getAsJsonArray().size();
count += currentResultSize;
token = result.get("nextPageToken") == null ? null : result.get("nextPageToken").getAsString();
@@ -893,7 +969,8 @@ public void testListAndGetForPaginatedAPIWithLatestOnlyLCMFlagEnabled() throws E
isLastPage = false;
currentResultSize = 0;
while (!isLastPage) {
- JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token, AllProgramsApp.NAME, null, false, null);
+ JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token,
+ AllProgramsApp.NAME, null, false, null, false);
currentResultSize = result.get("applications").getAsJsonArray().size();
count += currentResultSize;
token = result.get("nextPageToken") == null ? null : result.get("nextPageToken").getAsString();
@@ -931,7 +1008,8 @@ public void testListAndGetLCMFlagEnabledForVersionHistory() throws Exception {
boolean isLastPage = false;
int currentResultSize = 0;
while (!isLastPage) {
- JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token, AllProgramsApp.NAME, null, true, null);
+ JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token,
+ AllProgramsApp.NAME, null, true, null, false);
currentResultSize = result.get("applications").getAsJsonArray().size();
count += currentResultSize;
token = result.get("nextPageToken") == null ? null : result.get("nextPageToken").getAsString();
@@ -947,7 +1025,7 @@ public void testListAndGetLCMFlagEnabledForVersionHistory() throws Exception {
List creationTimeList = new ArrayList<>();
while (!isLastPage) {
JsonObject result = getAppListForPaginatedApi(TEST_NAMESPACE1, 3, token,
- "DESC", AllProgramsApp.NAME, "EQUALS", false, true);
+ "DESC", AllProgramsApp.NAME, "EQUALS", false, true, false);
currentResultSize = result.get("applications").getAsJsonArray().size();
count += currentResultSize;
token = result.get("nextPageToken") == null ? null : result.get("nextPageToken").getAsString();
@@ -1516,6 +1594,33 @@ public void testDeployAppWithDisabledProfileInSchedule() throws Exception {
deleteArtifact(artifactId, 200);
}
+ @Test
+ public void testGetApplicationCount() throws Exception {
+ for (int i = 0; i < 10; i++) {
+ String ns1AppName = AllProgramsApp.NAME + i;
+ Id.Namespace ns1 = Id.Namespace.from(TEST_NAMESPACE1);
+ Id.Artifact ns1ArtifactId = Id.Artifact.from(ns1, AllProgramsApp.class.getSimpleName(),
+ "1.0.0-SNAPSHOT");
+
+ HttpResponse response = addAppArtifact(ns1ArtifactId, AllProgramsApp.class);
+ Assert.assertEquals(200, response.getResponseCode());
+ Id.Application appId = Id.Application.from(ns1, ns1AppName);
+ response = deploy(appId,
+ new AppRequest<>(ArtifactSummary.from(ns1ArtifactId.toArtifactId())));
+ Assert.assertEquals(200, response.getResponseCode());
+ }
+
+ long result = getAppCount(TEST_NAMESPACE1);
+ Assert.assertEquals(10, result);
+
+ HttpResponse response = doDelete(getVersionedApiPath("apps/",
+ Constants.Gateway.API_VERSION_3_TOKEN, TEST_NAMESPACE1));
+ Assert.assertEquals(200, response.getResponseCode());
+
+ result = getAppCount(TEST_NAMESPACE1);
+ Assert.assertEquals(0, result);
+ }
+
@After
public void cleanup() throws Exception {
setLCMFlag(false);
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/MonitorHandlerTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/MonitorHandlerTest.java
index f3587dc7360a..9e875adb30ef 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/MonitorHandlerTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/services/http/handlers/MonitorHandlerTest.java
@@ -66,7 +66,7 @@ public void testSystemServices() throws Exception {
List actual = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()),
Charsets.UTF_8), token);
- Assert.assertEquals(10, actual.size());
+ Assert.assertEquals(9, actual.size());
urlConn.disconnect();
}
@@ -81,7 +81,7 @@ public void testSystemServicesStatus() throws Exception {
Map result = GSON.fromJson(new String(ByteStreams.toByteArray(urlConn.getInputStream()),
Charsets.UTF_8), token);
- Assert.assertEquals(10, result.size());
+ Assert.assertEquals(9, result.size());
urlConn.disconnect();
Assert.assertEquals("OK", result.get(Constants.Service.APP_FABRIC_HTTP));
}
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/AppMetadataStoreTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/AppMetadataStoreTest.java
index ab80eccd8eb8..81a8a2bc940d 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/AppMetadataStoreTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/AppMetadataStoreTest.java
@@ -24,6 +24,12 @@
import io.cdap.cdap.AllProgramsApp;
import io.cdap.cdap.api.app.ApplicationSpecification;
import io.cdap.cdap.api.artifact.ArtifactId;
+import io.cdap.cdap.api.plugin.Plugin;
+import io.cdap.cdap.api.schedule.SchedulableProgramType;
+import io.cdap.cdap.api.workflow.ScheduleProgramInfo;
+import io.cdap.cdap.api.workflow.WorkflowActionNode;
+import io.cdap.cdap.api.workflow.WorkflowNode;
+import io.cdap.cdap.api.workflow.WorkflowSpecification;
import io.cdap.cdap.app.store.ScanApplicationsRequest;
import io.cdap.cdap.common.app.RunIds;
import io.cdap.cdap.common.utils.ProjectInfo;
@@ -32,6 +38,7 @@
import io.cdap.cdap.internal.app.DefaultApplicationSpecification;
import io.cdap.cdap.internal.app.deploy.Specifications;
import io.cdap.cdap.internal.app.runtime.SystemArguments;
+import io.cdap.cdap.internal.app.store.plugin.Plugins;
import io.cdap.cdap.proto.ProgramRunStatus;
import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.artifact.ChangeDetail;
@@ -60,6 +67,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
@@ -861,7 +869,7 @@ public void testDuplicateWritesIgnored() throws Exception {
byte[] sourceId = new byte[] { 0 };
TransactionRunners.run(transactionRunner, context -> {
AppMetadataStore store = AppMetadataStore.create(context);
- assertSecondCallIsNull(() -> store.recordProgramProvisioning(runId, null, SINGLETON_PROFILE_MAP,
+ Assert.assertNotNull(store.recordProgramProvisioning(runId, null, SINGLETON_PROFILE_MAP,
sourceId, ARTIFACT_ID));
assertSecondCallIsNull(() -> store.recordProgramProvisioned(runId, 0, sourceId));
assertSecondCallIsNull(() -> store.recordProgramStart(runId, null, Collections.emptyMap(), sourceId));
@@ -1101,6 +1109,49 @@ public void testBatchApplications() {
}
}
+ @Test
+ public void testGetApplicationCount() {
+ ApplicationSpecification appSpec = Specifications.from(new AllProgramsApp());
+ NamespaceId customNamespace = new NamespaceId("custom");
+ createMultipleApplications(NamespaceId.DEFAULT, "test-default", 20, appSpec);
+ createMultipleApplications(NamespaceId.SYSTEM, "test-system", 5, appSpec);
+ createMultipleApplications(customNamespace, "test-custom", 15, appSpec);
+
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore store = AppMetadataStore.create(context);
+ long count = store.getApplicationCount();
+ // System apps are not included in the count. Default(20) and custom(15) are included.
+ Assert.assertEquals(35, count);
+ });
+
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore store = AppMetadataStore.create(context);
+ // System namespace has 20 applications.
+ long count = store.getNamespaceApplicationCount(NamespaceId.DEFAULT);
+ Assert.assertEquals(20, count);
+ // System namespace has 5 applications.
+ count = store.getNamespaceApplicationCount(NamespaceId.SYSTEM);
+ Assert.assertEquals(5, count);
+ // Custom namespace has 15 applications.
+ count = store.getNamespaceApplicationCount(customNamespace);
+ Assert.assertEquals(15, count);
+ });
+
+ }
+
+ private void createMultipleApplications(NamespaceId namespaceId, String appPrefix, int count,
+ ApplicationSpecification appSpec) {
+ for (int i = 0; i < count; i++) {
+ String appName = appPrefix + i;
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore store = AppMetadataStore.create(context);
+ store.writeApplication(namespaceId.getNamespace(), appName, ApplicationId.DEFAULT_VERSION,
+ appSpec,
+ new ChangeDetail(null, null, null, creationTimeMillis), null);
+ });
+ }
+ }
+
@Test
public void testScanApplications() {
ApplicationSpecification appSpec = Specifications.from(new AllProgramsApp());
@@ -1617,6 +1668,64 @@ public void testConcurrentCreateAppAfterTheFirstVersion() throws Exception {
Assert.assertEquals(1 + numThreads, appEditNumber.get());
}
+ @Test
+ public void testCreateAndGetApplication() {
+ String appName = "application1";
+ ArtifactId artifactId = NamespaceId.DEFAULT.artifact("testArtifact", "1.0").toApiArtifactId();
+ ApplicationReference appRef = new ApplicationReference(NamespaceId.DEFAULT, appName);
+ ApplicationId appId = appRef.app(appName + "_version_" + 1);
+ ApplicationSpecification spec = createDummyAppSpecWithWorkflow(appId, artifactId, false);
+ ApplicationMeta meta = new ApplicationMeta(spec.getName(), spec,
+ new ChangeDetail(null, null, null,
+ creationTimeMillis + 1, true));
+
+ // Deploy the first version
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore metaStore = AppMetadataStore.create(context);
+ StructuredTable pluginDataTable = metaStore.getPluginDataTable();
+ Plugins.addWranglerPluginToTable(pluginDataTable);
+ Plugins.addNowDirectivePluginToTable(pluginDataTable);
+ Plugins.addFormatTextPluginToTable(pluginDataTable);
+ metaStore.createLatestApplicationVersion(appId, meta);
+ });
+
+ // Verify latest version
+ final ApplicationMeta[] gotMeta = {null};
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore metaStore = AppMetadataStore.create(context);
+ gotMeta[0] = metaStore.getApplication(appId);
+ });
+
+ Assert.assertEquals(meta.toString(), gotMeta[0].toString());
+ }
+
+ @Test
+ public void testCreateAndGetApplicationWithMissingPlugins() {
+ String appName = "application1";
+ ArtifactId artifactId = NamespaceId.DEFAULT.artifact("testArtifact", "1.0").toApiArtifactId();
+ ApplicationReference appRef = new ApplicationReference(NamespaceId.DEFAULT, appName);
+ ApplicationId appId = appRef.app(appName + "_version_" + 1);
+ ApplicationSpecification spec = createDummyAppSpecWithWorkflow(appId, artifactId, true);
+ ApplicationMeta meta = new ApplicationMeta(spec.getName(), spec,
+ new ChangeDetail(null, null, null,
+ creationTimeMillis + 1, true));
+
+ // Deploy the first version
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore metaStore = AppMetadataStore.create(context);
+ StructuredTable pluginDataTable = metaStore.getPluginDataTable();
+ Plugins.addFormatTextPluginToTable(pluginDataTable);
+ metaStore.createLatestApplicationVersion(appId, meta);
+ });
+
+ // Verify latest version
+ TransactionRunners.run(transactionRunner, context -> {
+ AppMetadataStore metaStore = AppMetadataStore.create(context);
+ ApplicationMeta gotMeta = metaStore.getApplication(appId);
+ Assert.assertEquals(meta.toString(), Objects.requireNonNull(gotMeta).toString());
+ });
+ }
+
@Test
public void testDeleteCompletedRunsStartedBefore() throws Exception {
// Map an iterator to one of 15 different program+workflow permutations. Used to ensure
@@ -1757,6 +1866,31 @@ private ApplicationSpecification createDummyAppSpec(String appName, String appVe
Collections.emptyMap());
}
+ private ApplicationSpecification createDummyAppSpecWithWorkflow(ApplicationId appId,
+ ArtifactId artifactId, boolean isAppSpecReductionEnabled) {
+ Map plugins =
+ isAppSpecReductionEnabled ? Plugins.createDummyPluginsForReducedAppSpec()
+ : Plugins.createDummyPlugins();
+ ImmutableList nodes = ImmutableList.of(
+ new WorkflowActionNode("mr1",
+ new ScheduleProgramInfo(SchedulableProgramType.MAPREDUCE, "mr1")),
+ new WorkflowActionNode("spark1",
+ new ScheduleProgramInfo(SchedulableProgramType.SPARK, "spark1")));
+ WorkflowSpecification wfSpec =
+ new WorkflowSpecification("test", "wf1", "", Collections.emptyMap(),
+ nodes,
+ Collections.emptyMap(), plugins);
+ return new DefaultApplicationSpecification(
+ appId.getApplication(), appId.getVersion(), ProjectInfo.getVersion().toString(), "desc",
+ null, artifactId,
+ Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(),
+ Collections.emptyMap(),
+ ImmutableMap.of(appId.workflow("wf1").getProgram(), wfSpec), Collections.emptyMap(),
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ plugins);
+ }
+
private void runConcurrentOperation(String name, int numThreads, Runnable runnable) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
CountDownLatch startLatch = new CountDownLatch(1);
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/DefaultStoreTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/DefaultStoreTest.java
index 3173d0dee397..29054c9b0c62 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/DefaultStoreTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/DefaultStoreTest.java
@@ -435,7 +435,9 @@ public void testLogProgramRunHistory() {
.setProperties(noRuntimeArgsProps)
.setCluster(emptyCluster)
.setArtifactId(artifactId)
- .setSourceId(AppFabricTestHelper.createSourceId(sourceId)).build();
+ .setSourceId(AppFabricTestHelper.createSourceId(sourceId))
+ .setFlowControlStatus("")
+ .build();
RunRecordDetail actualRecord7 = store.getRun(programId.run(run7.getId()));
Assert.assertEquals(expectedRunRecord7, actualRecord7);
@@ -450,7 +452,9 @@ public void testLogProgramRunHistory() {
.setProperties(noRuntimeArgsProps)
.setCluster(emptyCluster)
.setArtifactId(artifactId)
- .setSourceId(AppFabricTestHelper.createSourceId(sourceId)).build();
+ .setSourceId(AppFabricTestHelper.createSourceId(sourceId))
+ .setFlowControlStatus("")
+ .build();
RunRecordDetail actualRecord8 = store.getRun(programId.run(run8.getId()));
Assert.assertEquals(expectedRunRecord8, actualRecord8);
@@ -470,7 +474,9 @@ public void testLogProgramRunHistory() {
.setProperties(noRuntimeArgsProps)
.setCluster(emptyCluster)
.setArtifactId(artifactId)
- .setSourceId(AppFabricTestHelper.createSourceId(sourceId)).build();
+ .setSourceId(AppFabricTestHelper.createSourceId(sourceId))
+ .setFlowControlStatus("")
+ .build();
RunRecordDetail actualRecord9 = store.getRun(programId.run(run9.getId()));
Assert.assertEquals(expectedRunRecord9, actualRecord9);
@@ -1446,6 +1452,23 @@ public void testListRunsWithLegacyRows() throws ConflictException {
Assert.assertEquals(latestAppId.get(), actualApps.get(0));
}
+ @Test
+ public void testGetApplicationCount() throws ConflictException {
+ ApplicationSpecification spec = Specifications.from(new AllProgramsApp());
+ NamespaceId namespaceId = new NamespaceId("custom");
+ ApplicationMeta appMeta = new ApplicationMeta(spec.getName(), spec,
+ new ChangeDetail(null, null, null,
+ System.currentTimeMillis()));
+ for (int i = 0; i < 5; i++) {
+ ApplicationId appId = namespaceId.app("application" + i);
+ store.addLatestApplication(appId, appMeta);
+ Assert.assertNotNull(store.getApplication(appId));
+ }
+ long result = store.getApplicationCount(namespaceId);
+
+ Assert.assertEquals(5, result);
+ }
+
private void writeStartRecord(ProgramRunId run, ArtifactId artifactId) {
setStartAndRunning(run, artifactId);
Assert.assertNotNull(store.getRun(run));
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/SqlAppMetadataStoreTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/SqlAppMetadataStoreTest.java
index 8593b7ba59ad..b85e75d94486 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/SqlAppMetadataStoreTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/SqlAppMetadataStoreTest.java
@@ -68,6 +68,7 @@ protected void configure() {
transactionRunner = injector.getInstance(TransactionRunner.class);
StoreDefinition.AppMetadataStore.create(injector.getInstance(StructuredTableAdmin.class));
+ StoreDefinition.ArtifactStore.create(injector.getInstance(StructuredTableAdmin.class));
}
@AfterClass
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/plugin/Plugins.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/plugin/Plugins.java
new file mode 100644
index 000000000000..2f0125b257b9
--- /dev/null
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/plugin/Plugins.java
@@ -0,0 +1,209 @@
+/*
+ * 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.store.plugin;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.Gson;
+import io.cdap.cdap.api.artifact.ArtifactId;
+import io.cdap.cdap.api.plugin.Plugin;
+import io.cdap.cdap.api.plugin.PluginClass;
+import io.cdap.cdap.api.plugin.PluginProperties;
+import io.cdap.cdap.api.plugin.PluginPropertyField;
+import io.cdap.cdap.api.plugin.Requirements;
+import io.cdap.cdap.proto.id.NamespaceId;
+import io.cdap.cdap.spi.data.StructuredTable;
+import io.cdap.cdap.spi.data.table.field.Field;
+import io.cdap.cdap.spi.data.table.field.Fields;
+import io.cdap.cdap.store.StoreDefinition.ArtifactStore;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Util for building plugins for tests.
+ */
+public final class Plugins {
+
+ private Plugins() {
+ }
+
+ /**
+ * Creates dummy plugins.
+ */
+ public static Map createDummyPlugins() {
+ Map plugins = new HashMap<>();
+
+ Gson gson = new Gson();
+ Map childProperties = ImmutableMap.of("child1", "childVal1", "child2",
+ "${secure(acc)}", "child3", "val3");
+ Map properties = ImmutableMap.of("key2", gson.toJson(childProperties), "key1",
+ "val1");
+
+ PluginClass wranglerPluginClass = PluginClass.builder()
+ .setClassName("io.cdap.wrangler.Wrangler").setName("Wrangler").setConfigFieldName("config")
+ .setRequirements(Requirements.EMPTY).setType("transform")
+ .setDescription("Wrangler - A interactive tool for data cleansing and transformation.")
+ .add("schema",
+ new PluginPropertyField("schema", "Specifies the schema that has to be output.",
+ "string", true, true)).add("preconditionSQL",
+ new PluginPropertyField("preconditionSQL",
+ "SQL Precondition expression specifying filtering before applying directives (false to filter)",
+ "string", false, true, false)).build();
+ Plugin wranglerPlugin = new Plugin(Collections.emptyList(),
+ NamespaceId.DEFAULT.artifact("wrangler-transform", "1.0").toApiArtifactId(),
+ wranglerPluginClass, PluginProperties.builder().addAll(properties).build());
+ plugins.put("p1", wranglerPlugin);
+
+ PluginClass directivePluginClass = PluginClass.builder()
+ .setClassName("com.google.cloud.datafusion.directives.Now").setName("now")
+ .setRequirements(Requirements.EMPTY).setType("directive")
+ .setDescription("Populates a column with the current date and time").build();
+ ArtifactId parent = NamespaceId.DEFAULT.artifact("Wrangler", "1.0").toApiArtifactId();
+ Plugin directivePlugin = new Plugin(Collections.singleton(parent),
+ NamespaceId.DEFAULT.artifact("wrangler", "1.0").toApiArtifactId(), directivePluginClass,
+ PluginProperties.builder().addAll(properties).build());
+ plugins.put("p2", directivePlugin);
+
+ PluginClass gCloudFormatTextPluginClass = PluginClass.builder()
+ .setClassName("io.cdap.plugin.format.text.input.TextInputFormatProvider").setName("text")
+ .setRequirements(Requirements.EMPTY).setType("validatingInputFormat")
+ .setDescription("Plugin for reading files in text format.").setConfigFieldName("conf")
+ .build();
+ parent = NamespaceId.DEFAULT.artifact("google-cloud", "1.0").toApiArtifactId();
+ Plugin gCloudFormatTextPlugin = new Plugin(Collections.singleton(parent),
+ NamespaceId.DEFAULT.artifact("format-text", "1.0").toApiArtifactId(),
+ gCloudFormatTextPluginClass, PluginProperties.builder().addAll(properties).build());
+ plugins.put("p3", gCloudFormatTextPlugin);
+
+ return plugins;
+ }
+
+ public static Map createDummyPluginsForReducedAppSpec() {
+ Map plugins = new HashMap<>();
+
+ Gson gson = new Gson();
+ Map childProperties = ImmutableMap.of("child1", "childVal1", "child2",
+ "${secure(acc)}", "child3", "val3");
+ Map properties = ImmutableMap.of("key2", gson.toJson(childProperties), "key1",
+ "val1");
+
+ PluginClass wranglerPluginClass = new PluginClass("transform", "Wrangler", null, null,
+ Collections.emptyMap(), new Requirements(Collections.emptySet()), "");
+ Plugin wranglerPlugin = new Plugin(Collections.emptyList(),
+ NamespaceId.DEFAULT.artifact("wrangler-transform", "1.0").toApiArtifactId(),
+ wranglerPluginClass, PluginProperties.builder().addAll(properties).build());
+ plugins.put("p1", wranglerPlugin);
+
+ PluginClass directivePluginClass = new PluginClass("directive", "now", null, null,
+ Collections.emptyMap(), new Requirements(Collections.emptySet()), "");
+ ArtifactId parent = NamespaceId.DEFAULT.artifact("Wrangler", "1.0").toApiArtifactId();
+ Plugin directivePlugin = new Plugin(Collections.singleton(parent),
+ NamespaceId.DEFAULT.artifact("wrangler", "1.0").toApiArtifactId(), directivePluginClass,
+ PluginProperties.builder().addAll(properties).build());
+ plugins.put("p2", directivePlugin);
+
+ PluginClass gCloudFormatTextPluginClass = PluginClass.builder()
+ .setClassName("io.cdap.plugin.format.text.input.TextInputFormatProvider").setName("text")
+ .setRequirements(Requirements.EMPTY).setType("validatingInputFormat")
+ .setDescription("Plugin for reading files in text format.").setConfigFieldName("conf")
+ .build();
+ parent = NamespaceId.DEFAULT.artifact("google-cloud", "1.0").toApiArtifactId();
+ Plugin gCloudFormatTextPlugin = new Plugin(Collections.singleton(parent),
+ NamespaceId.DEFAULT.artifact("format-text", "1.0").toApiArtifactId(),
+ gCloudFormatTextPluginClass, PluginProperties.builder().addAll(properties).build());
+ plugins.put("p3", gCloudFormatTextPlugin);
+
+ return plugins;
+ }
+
+ private static void addPluginEntryToTable(StructuredTable pluginDataTable,
+ String parentContextName, String pluginType, String pluginName, String artifactName,
+ String pluginDataJson) throws IOException {
+ Collection> fields = new ArrayList<>();
+ fields.add(Fields.stringField(ArtifactStore.PARENT_NAMESPACE_FIELD, "default"));
+ fields.add(Fields.stringField(ArtifactStore.PARENT_NAME_FIELD, parentContextName));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_TYPE_FIELD, pluginType));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_NAME_FIELD, pluginName));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_NAMESPACE_FIELD, "default"));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_NAME_FIELD, artifactName));
+ fields.add(Fields.stringField(ArtifactStore.ARTIFACT_VER_FIELD, "1.0"));
+ fields.add(Fields.stringField(ArtifactStore.PLUGIN_DATA_FIELD, pluginDataJson));
+ pluginDataTable.upsert(fields);
+ }
+
+ /**
+ * Initializes a {@link StructuredTable} with a dummy entry for a Wrangler plugin.
+ */
+ public static void addWranglerPluginToTable(StructuredTable pluginDataTable) throws IOException {
+ String wranglerPluginJsonData = "{\"pluginClass\":{\"type\":"
+ + "\"transform\",\"name\":\"Wrangler\",\"description\":\"Wrangler - A interactive tool "
+ + "for data cleansing and transformation.\",\"className\":\"io.cdap.wrangler.Wrangler\","
+ + "\"configFieldName\":\"config\",\"properties\":{\"schema\":{\"name\":\"schema\",\"description"
+ + "\":\"Specifies the schema that has to be output.\",\"type\":\"string\",\"required\":true,"
+ + "\"macroSupported\":true,\"macroEscapingEnabled\":false,\"children\":[]},\"preconditionSQL\""
+ + ":{\"name\":\"preconditionSQL\",\"description\":\"SQL Precondition expression specifying"
+ + " filtering before applying directives (false to filter)\",\"type\":\"string\",\"required"
+ + "\":false,\"macroSupported\":true,\"macroEscapingEnabled\":false,\"children\":[]}},"
+ + "\"requirements\":{\"datasetTypes\":[],\""
+ + "capabilities\":[]}},\"artifactLocationPath\":\"/cdap/namespaces/system/artifacts/wrangler-transform"
+ + "/4.11.0-SNAPSHOT.d26c4ac8-600a-4bf0-8280-0561037036d8.jar\",\"usableBy\":\""
+ + "system:cdap-data-pipeline[6.10.0,7.0.0-SNAPSHOT)\"}"; // Example path/usableBy
+
+ addPluginEntryToTable(pluginDataTable, "testArtifact", "transform", "Wrangler",
+ "wrangler-transform", wranglerPluginJsonData);
+ }
+
+ /**
+ * Initializes a {@link StructuredTable} with a dummy entry for a "now" directive plugin.
+ */
+ public static void addNowDirectivePluginToTable(StructuredTable pluginDataTable)
+ throws IOException {
+
+ ArtifactId parent = NamespaceId.DEFAULT.artifact("Wrangler", "1.0").toApiArtifactId();
+ String nowDirectiveJsonData = "{\"pluginClass\":{\"type\":\"directive\",\"name\":\"now\","
+ + "\"description\":\"Populates a column with the current date and time\","
+ + "\"className\":\"com.google.cloud.datafusion.directives.Now\",\"properties\":{},"
+ + "\"requirements\":{\"datasetTypes\":[],\"capabilities\":[]}},"
+ + "\"artifactLocationPath\":\"/cdap/namespaces/default/artifacts/now-directive/1.0.0-SNAPSHOT.jar\","
+ + "\"usableBy\":\"system:wrangler-transform[4.0.0,5.0.0)\"}";
+
+ addPluginEntryToTable(pluginDataTable, parent.getName(), "directive", "now", "wrangler",
+ nowDirectiveJsonData);
+ }
+
+ /**
+ * Initializes a {@link StructuredTable} with a dummy entry for a "format-text" plugin.
+ */
+ public static void addFormatTextPluginToTable(StructuredTable pluginDataTable)
+ throws IOException {
+
+ String formatTextJsonData =
+ "{\"pluginClass\":{\"type\":\"validatingInputFormat\",\"name\":\"text\","
+ + "\"description\":\"Plugin for reading files in text format.\","
+ + "\"className\":\"io.cdap.plugin.format.text.input.TextInputFormatProvider\","
+ + "\"configFieldName\":\"conf\",\"requirements\":{\"datasetTypes\":[],"
+ + "\"capabilities\":[]}},\"artifactLocationPath\":\"/cdap/namespaces/system/artifacts/format"
+ + "-text/2.14.0-SNAPSHOT.4fed59ac-ef3f-4b76-ac29-d5aa8bc38061.jar\","
+ + "\"usableBy\":\"system:cdap-data-pipeline[6.8.0-SNAPSHOT,7.0.0-SNAPSHOT)\"}";
+
+ addPluginEntryToTable(pluginDataTable, "testArtifact", "validatingInputFormat", "text",
+ "format-text", formatTextJsonData);
+ }
+}
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStoreTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStoreTest.java
index 59a8c171372f..8cdfe8e1a048 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStoreTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/store/preview/DefaultPreviewStoreTest.java
@@ -69,7 +69,7 @@ public static void beforeClass() throws IOException {
cConf.set(Constants.CFG_LOCAL_DATA_DIR, TEMP_FOLDER.newFolder().getAbsolutePath());
Injector injector = Guice.createInjector(
- new PreviewConfigModule(cConf, new Configuration(), SConfiguration.create())
+ new PreviewConfigModule(cConf, new Configuration(), SConfiguration.create())
);
store = injector.getInstance(DefaultPreviewStore.class);
}
@@ -87,10 +87,12 @@ public void before() throws Exception {
@Test
public void testPreviewStore() {
String firstApplication = RunIds.generate().getId();
- ApplicationId firstApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(), firstApplication);
+ ApplicationId firstApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(),
+ firstApplication);
String secondApplication = RunIds.generate().getId();
- ApplicationId secondApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(), secondApplication);
+ ApplicationId secondApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(),
+ secondApplication);
// put data for the first application
store.put(firstApplicationId, "mytracer", "key1", "value1");
@@ -113,10 +115,12 @@ public void testPreviewStore() {
Assert.assertEquals(2, firstApplicationData.get("key1").get(1).getAsInt());
Assert.assertEquals(3, firstApplicationData.get("key2").get(0).getAsInt());
Assert.assertEquals(propertyMap, GSON.fromJson(firstApplicationData.get("key2").get(1),
- new TypeToken>() { }.getType()));
+ new TypeToken>() {
+ }.getType()));
// get the data for second application and logger name "mytracer"
- Map> secondApplicationData = store.get(secondApplicationId, "mytracer");
+ Map> secondApplicationData = store.get(secondApplicationId,
+ "mytracer");
Assert.assertEquals(1, secondApplicationData.size());
Assert.assertEquals("value1", secondApplicationData.get("key1").get(0).getAsString());
@@ -136,9 +140,10 @@ public void testPreviewInfo() throws IOException {
// test put and get
ApplicationId applicationId = new ApplicationId("ns1", "app1");
ProgramRunId runId = new ProgramRunId("ns1", "app1", ProgramType.WORKFLOW, "test",
- RunIds.generate().getId());
- PreviewStatus status = new PreviewStatus(PreviewStatus.Status.COMPLETED, System.currentTimeMillis(), null, 0L,
- System.currentTimeMillis());
+ RunIds.generate().getId());
+ PreviewStatus status = new PreviewStatus(PreviewStatus.Status.COMPLETED,
+ System.currentTimeMillis(), null, 0L,
+ System.currentTimeMillis());
store.setProgramId(runId);
store.setPreviewStatus(applicationId, status);
@@ -151,7 +156,8 @@ public void testPreviewWaitingRequests() throws Exception {
byte[] pollerInfo = Bytes.toBytes("runner-1");
PreviewConfig previewConfig = new PreviewConfig("WordCount", ProgramType.WORKFLOW, null, null);
- AppRequest> testRequest = new AppRequest<>(new ArtifactSummary("test", "1.0"), null, previewConfig);
+ AppRequest> testRequest = new AppRequest<>(new ArtifactSummary("test", "1.0"), null,
+ previewConfig);
Assert.assertEquals(0, store.getAllInWaitingState().size());
RunId id1 = RunIds.generate();
@@ -191,9 +197,9 @@ public void testPreviewWaitingRequests() throws Exception {
// Add a preview request that has a principle associated with it
ApplicationId applicationId4 = new ApplicationId("ns1", RunIds.generate().getId());
Principal principal = new Principal("userForApplicationId4",
- Principal.PrincipalType.USER,
- new Credential("userForApplicationId4Credential",
- Credential.CredentialType.EXTERNAL));
+ Principal.PrincipalType.USER,
+ new Credential("userForApplicationId4Credential",
+ Credential.CredentialType.EXTERNAL));
store.add(applicationId4, testRequest, principal);
allWaiting = store.getAllInWaitingState();
Assert.assertEquals(1, allWaiting.size());
@@ -207,16 +213,20 @@ public void testPreviewWaitingRequests() throws Exception {
@Test
public void testPreviewTTL() throws Exception {
PreviewConfig previewConfig = new PreviewConfig("WordCount", ProgramType.WORKFLOW, null, null);
- AppRequest> testRequest = new AppRequest<>(new ArtifactSummary("test", "1.0"), null, previewConfig);
+ AppRequest> testRequest = new AppRequest<>(new ArtifactSummary("test", "1.0"), null,
+ previewConfig);
String firstApplication = RunIds.generate(System.currentTimeMillis() - 5000).getId();
- ApplicationId firstApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(), firstApplication);
+ ApplicationId firstApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(),
+ firstApplication);
String secondApplication = RunIds.generate(System.currentTimeMillis() - 4000).getId();
- ApplicationId secondApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(), secondApplication);
+ ApplicationId secondApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(),
+ secondApplication);
String thirdApplication = RunIds.generate(System.currentTimeMillis()).getId();
- ApplicationId thirdApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(), thirdApplication);
+ ApplicationId thirdApplicationId = new ApplicationId(NamespaceMeta.DEFAULT.getName(),
+ thirdApplication);
store.add(firstApplicationId, testRequest, null);
store.add(secondApplicationId, testRequest, null);
@@ -246,6 +256,39 @@ public void testPreviewTTL() throws Exception {
// only thirdApplication should be in waiting now
Assert.assertEquals(1, store.getAllInWaitingState().size());
- Assert.assertEquals(thirdApplicationId, store.getAllInWaitingState().iterator().next().getProgram().getParent());
+ Assert.assertEquals(thirdApplicationId,
+ store.getAllInWaitingState().iterator().next().getProgram().getParent());
+ }
+
+ @Test
+ public void testGetApplicationIdForPollerInfo() throws Exception {
+ byte[] pollerInfo1 = Bytes.toBytes("runner-1");
+ byte[] pollerInfo2 = Bytes.toBytes("runner-2");
+
+ PreviewConfig previewConfig = new PreviewConfig("WordCount", ProgramType.WORKFLOW, null, null);
+ AppRequest> testRequest = new AppRequest<>(new ArtifactSummary("test", "1.0"), null,
+ previewConfig);
+
+ // 1. Test success case: associate pollerInfo1 with applicationId1
+ ApplicationId applicationId1 = new ApplicationId("ns1", RunIds.generate().getId());
+ store.add(applicationId1, testRequest, null);
+ store.setPreviewRequestPollerInfo(applicationId1, pollerInfo1);
+
+ // Verify that the mapping is correct
+ Assert.assertEquals(applicationId1, store.getApplicationId(pollerInfo1));
+
+ // 2. Test failure case: pollerInfo2 is not associated with any application
+ Assert.assertNull(store.getApplicationId(pollerInfo2));
+
+ // 3. Test re-association: associate pollerInfo1 with a new application
+ // This simulates a scenario where a runner with the same info is reused, overwriting the index.
+ ApplicationId applicationId2 = new ApplicationId("ns1", RunIds.generate().getId());
+ store.add(applicationId2, testRequest, null);
+ store.setPreviewRequestPollerInfo(applicationId2, pollerInfo1);
+ Assert.assertEquals(applicationId2, store.getApplicationId(pollerInfo1));
+
+ // 4. Test removal: remove the application and verify the mapping is gone
+ store.remove(applicationId2);
+ Assert.assertNull(store.getApplicationId(pollerInfo1));
}
}
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceTest.java
index 0f757e5e4ea7..a701045a37fe 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/app/worker/TaskWorkerServiceTest.java
@@ -72,6 +72,8 @@ public class TaskWorkerServiceTest {
private TaskWorkerService taskWorkerService;
private CompletableFuture serviceCompletionFuture;
+ private SecurityManager securityManager;
+
private CConfiguration createCConf() {
CConfiguration cConf = CConfiguration.create();
cConf.set(Constants.TaskWorker.ADDRESS, "localhost");
@@ -88,7 +90,7 @@ private SConfiguration createSConf() {
}
@Before
- public void beforeTest() {
+ public void beforeTest() throws Exception {
CConfiguration cConf = createCConf();
SConfiguration sConf = createSConf();
@@ -102,6 +104,8 @@ public void beforeTest() {
// start the service
taskWorkerService.startAndWait();
this.taskWorkerService = taskWorkerService;
+ securityManager = System.getSecurityManager();
+ System.setSecurityManager(new NoExitSecurityManager());
}
@After
@@ -110,6 +114,7 @@ public void afterTest() {
taskWorkerService.stopAndWait();
taskWorkerService = null;
}
+ System.setSecurityManager(securityManager);
}
@Test
@@ -496,4 +501,17 @@ public void run(RunnableTaskContext context) throws Exception {
context.writeResult(context.getParam().getBytes(StandardCharsets.UTF_8));
}
}
+
+ public class NoExitSecurityManager extends SecurityManager {
+
+ @Override
+ public void checkExit(int status) {
+ throw new SecurityException("System.exit attempted, status=" + status);
+ }
+
+ @Override
+ public void checkPermission(java.security.Permission perm) {
+ // allow everything else
+ }
+ }
}
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-app-fabric/src/test/java/io/cdap/cdap/internal/events/StartProgramEventSubscriberTest.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/events/StartProgramEventSubscriberTest.java
index 83f59b7e16f6..b36b6a0cb356 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/events/StartProgramEventSubscriberTest.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/events/StartProgramEventSubscriberTest.java
@@ -27,7 +27,7 @@
import io.cdap.cdap.common.conf.CConfiguration;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.internal.app.services.ProgramLifecycleService;
-import io.cdap.cdap.internal.app.services.RunRecordMonitorService;
+import io.cdap.cdap.internal.app.services.FlowControlService;
import io.cdap.cdap.internal.app.services.http.AppFabricTestBase;
import io.cdap.cdap.internal.events.dummy.DummyEventReader;
import io.cdap.cdap.internal.events.dummy.DummyEventReaderExtensionProvider;
@@ -38,7 +38,6 @@
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;
-import org.mockito.Mock;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,8 +48,8 @@
public class StartProgramEventSubscriberTest extends AppFabricTestBase {
private static final Logger LOG = LoggerFactory.getLogger(StartProgramEventSubscriberTest.class);
private ProgramLifecycleService lifecycleService;
- private RunRecordMonitorService runRecordMonitorService;
- private RunRecordMonitorService.Counter mockCounter;
+ private FlowControlService flowControlService;
+ private FlowControlService.Counter mockCounter;
private CConfiguration cConf;
private DummyEventReader eventReader;
private Injector injector;
@@ -59,16 +58,16 @@ public class StartProgramEventSubscriberTest extends AppFabricTestBase {
@Before
public void setup() {
lifecycleService = Mockito.mock(ProgramLifecycleService.class);
- runRecordMonitorService = Mockito.mock(RunRecordMonitorService.class);
- mockCounter = Mockito.mock(RunRecordMonitorService.Counter.class);
- Mockito.doReturn(mockCounter).when(runRecordMonitorService).getCount();
+ flowControlService = Mockito.mock(FlowControlService.class);
+ mockCounter = Mockito.mock(FlowControlService.Counter.class);
+ Mockito.doReturn(mockCounter).when(flowControlService).getCounter();
cConf = CConfiguration.create();
eventReader = new DummyEventReader<>(mockedEvents());
injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(ProgramLifecycleService.class).toInstance(lifecycleService);
- bind(RunRecordMonitorService.class).toInstance(runRecordMonitorService);
+ bind(FlowControlService.class).toInstance(flowControlService);
bind(CConfiguration.class).toInstance(cConf);
bind(new TypeLiteral>() {
})
diff --git a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/guice/AppFabricTestModule.java b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/guice/AppFabricTestModule.java
index 45e75f1d9646..05231a57bb8d 100644
--- a/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/guice/AppFabricTestModule.java
+++ b/cdap-app-fabric/src/test/java/io/cdap/cdap/internal/guice/AppFabricTestModule.java
@@ -91,8 +91,9 @@ protected void configure() {
install(RemoteAuthenticatorModules.getNoOpModule());
install(new IOModule());
install(new InMemoryDiscoveryModule());
- install(new AppFabricServiceRuntimeModule(cConf).getInMemoryModules());
- install(new MonitorHandlerModule(false));
+ install(new AppFabricServiceRuntimeModule(cConf, AppFabricServiceRuntimeModule.ALL_SERVICE_TYPES)
+ .getInMemoryModules());
+ install(new MonitorHandlerModule(false, cConf));
install(new ProgramRunnerRuntimeModule().getInMemoryModules());
install(new NonCustomLocationUnitTestModule());
install(new LocalLogAppenderModule());
diff --git a/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthAccessToken.java b/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthAccessToken.java
new file mode 100644
index 000000000000..7afff882947b
--- /dev/null
+++ b/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthAccessToken.java
@@ -0,0 +1,55 @@
+/*
+ * 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.datapipeline.oauth;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * OAuth access token, with related metadata required to retrieve a long-lived access token.
+ */
+public class OAuthAccessToken {
+ private final String accessToken;
+
+ public OAuthAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder class for {@link OAuthAccessToken}.
+ */
+ public static class Builder {
+ private String accessToken;
+
+ public Builder() {}
+
+ public Builder withAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ return this;
+ }
+
+ public OAuthAccessToken build() {
+ Preconditions.checkNotNull(accessToken, "OAuth access token missing");
+ return new OAuthAccessToken(accessToken);
+ }
+ }
+}
diff --git a/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthProvider.java b/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthProvider.java
index 9ba6b59e9358..0aeea2777325 100644
--- a/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthProvider.java
+++ b/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthProvider.java
@@ -26,18 +26,25 @@ public class OAuthProvider {
private final String tokenRefreshURL;
@Nullable
private final OAuthClientCredentials clientCreds;
+ @Nullable
private final CredentialEncodingStrategy strategy;
+ // Optional string to send as a USER_AGENT header
+ @Nullable
+ private final String userAgent;
+
public OAuthProvider(String name,
String loginURL,
String tokenRefreshURL,
@Nullable OAuthClientCredentials clientCreds,
- @Nullable CredentialEncodingStrategy strategy) {
+ @Nullable CredentialEncodingStrategy strategy,
+ @Nullable String userAgent) {
this.name = name;
this.loginURL = loginURL;
this.tokenRefreshURL = tokenRefreshURL;
this.clientCreds = clientCreds;
this.strategy = strategy;
+ this.userAgent = userAgent;
}
public String getName() {
@@ -57,12 +64,18 @@ public OAuthClientCredentials getClientCredentials() {
return clientCreds;
}
+ @Nullable
public CredentialEncodingStrategy getCredentialEncodingStrategy() {
return strategy;
}
+ @Nullable
+ public String getUserAgent() {
+ return userAgent;
+ }
+
public enum CredentialEncodingStrategy {
- // (default) Sends client ID & secret as part of the POST request body
+ // (default) Sends client ID & secret as part of the POST request body
FORM_BODY,
// Sends client ID & secret as part of a HTTP Basic Auth header
BASIC_AUTH,
@@ -81,6 +94,7 @@ public static class Builder {
private String tokenRefreshURL;
private OAuthClientCredentials clientCreds;
private CredentialEncodingStrategy strategy;
+ private String userAgent;
public Builder() {}
@@ -109,6 +123,11 @@ public Builder withCredentialEncodingStrategy(@Nullable CredentialEncodingStrate
return this;
}
+ public Builder withUserAgent(@Nullable String userAgent) {
+ this.userAgent = userAgent;
+ return this;
+ }
+
public OAuthProvider build() {
Preconditions.checkNotNull(name, "OAuth provider name missing");
Preconditions.checkNotNull(loginURL, "Login URL missing");
@@ -117,7 +136,7 @@ public OAuthProvider build() {
if (strategy == null) {
this.strategy = CredentialEncodingStrategy.FORM_BODY;
}
- return new OAuthProvider(name, loginURL, tokenRefreshURL, clientCreds, strategy);
+ return new OAuthProvider(name, loginURL, tokenRefreshURL, clientCreds, strategy, userAgent);
}
}
}
diff --git a/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthStore.java b/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthStore.java
index a1a9a9718090..27bd024fbdbd 100644
--- a/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthStore.java
+++ b/cdap-app-templates/cdap-etl/cdap-data-pipeline-base/src/main/java/io/cdap/cdap/datapipeline/oauth/OAuthStore.java
@@ -44,7 +44,10 @@ public class OAuthStore {
private static final String OAUTH_PROVIDER_COL = "oauthprovider";
private static final String LOGIN_URL_COL = "loginurl";
private static final String TOKEN_REFRESH_URL_COL = "tokenrefreshurl";
+ private static final String CREDENTIAL_ENCODING_STRATEGY_COL = "credentialencodingstrategy";
+ private static final String USER_AGENT_COL = "useragent";
private static final String CLIENT_CREDS_KEY_PREFIX = "oauthclientcreds";
+ private static final String ACCESS_TOKEN_KEY_PREFIX = "oauthaccesstoken";
private static final String REFRESH_TOKEN_KEY_PREFIX = "oauthrefreshtoken";
private static final Gson GSON = new Gson();
private final TransactionRunner transactionRunner;
@@ -56,7 +59,9 @@ public class OAuthStore {
.withId(TABLE_ID)
.withFields(Fields.stringType(OAUTH_PROVIDER_COL),
Fields.stringType(LOGIN_URL_COL),
- Fields.stringType(TOKEN_REFRESH_URL_COL))
+ Fields.stringType(TOKEN_REFRESH_URL_COL),
+ Fields.stringType(CREDENTIAL_ENCODING_STRATEGY_COL),
+ Fields.stringType(USER_AGENT_COL))
.withPrimaryKeys(OAUTH_PROVIDER_COL)
.build();
@@ -151,6 +156,35 @@ public Optional getProvider(String name) throws OAuthStoreExcepti
}
}
+ /**
+ * Remove an OAuth provider.
+ *
+ * @param name name of {@link OAuthProvider} to read
+ * @throws OAuthStoreException if the read fails
+ */
+ public void deleteProvider(String name) throws OAuthStoreException {
+ // Delete associated Client Credentials with given provider.
+ try {
+ secureStoreManager.delete(NamespaceId.SYSTEM.getNamespace(), getClientCredsKey(name));
+ } catch (Exception e) {
+ // If key is not found, then we can safely delete provider. For any other exception, throw it.
+ if (!e.getClass().getName().contains("NotFoundException")) {
+ throw new OAuthStoreException("Failed to delete client credential from OAuth provider secure storage", e);
+ }
+ }
+
+ try {
+ TransactionRunners.run(transactionRunner, context -> {
+ StructuredTable table = context.getTable(TABLE_ID);
+ table.delete(getKey(name));
+ }, TableNotFoundException.class, InvalidFieldException.class);
+ } catch (TableNotFoundException e) {
+ throw new OAuthStoreException("OAuth provider table not found", e);
+ } catch (InvalidFieldException e) {
+ throw new OAuthStoreException("Failed to delete OAuth provider, object fields do not match table", e);
+ }
+ }
+
/**
* Write an OAuth refresh token for the given provider and credential.
*
@@ -199,6 +233,57 @@ public Optional getRefreshToken(String oauthProvider, String
}
}
+ /**
+ * Write an OAuth access token for the given provider and credential. This is used for providers which do not provide
+ * a refresh token and instead opt for a permanent access token.
+ *
+ * @param oauthProvider name of OAuth provider the access token is sourced from
+ * @param credentialId ID used to identify this credential
+ * @param token the {@link OAuthAccessToken} to write
+ * @throws OAuthStoreException if the write fails
+ */
+ public void writeAccessToken(
+ String oauthProvider,
+ String credentialId,
+ OAuthAccessToken token) throws OAuthStoreException {
+ String namespace = NamespaceId.SYSTEM.getNamespace();
+ try {
+ secureStoreManager.put(
+ namespace,
+ getAccessTokenKey(oauthProvider, credentialId),
+ GSON.toJson(token),
+ "OAuth access token",
+ Collections.emptyMap());
+ } catch (IOException e) {
+ throw new OAuthStoreException("Failed to write OAuth access token", e);
+ } catch (Exception e) {
+ throw new OAuthStoreException("Namespace \"" + namespace + "\" does not exist", e);
+ }
+ }
+
+ /**
+ * Retrieve the {@link OAuthAccessToken} associated with the given OAuth provider and credential
+ *
+ * @param oauthProvider name of the OAuth provider the access token is sourced from
+ * @param credentialId ID used to identify this credential
+ * @throws OAuthStoreException if the read fails
+ */
+ public Optional getAccessToken(String oauthProvider, String credentialId)
+ throws OAuthStoreException {
+ try {
+ String tokenJson = new String(
+ secureStore.getData(NamespaceId.SYSTEM.getNamespace(), getAccessTokenKey(oauthProvider, credentialId)),
+ StandardCharsets.UTF_8);
+ return Optional.of(GSON.fromJson(tokenJson, OAuthAccessToken.class));
+ } catch (IOException e) {
+ throw new OAuthStoreException("Failed to read from OAuth access token secure storage", e);
+ } catch (JsonSyntaxException e) {
+ throw new OAuthStoreException("Invalid JSON for OAuth access token", e);
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
private static String getClientCredsKey(String oauthProvider) {
return String.format("%s-%s", CLIENT_CREDS_KEY_PREFIX, oauthProvider.toLowerCase());
}
@@ -207,6 +292,10 @@ private static String getRefreshTokenKey(String oauthProvider, String credential
return String.format("%s-%s-%s", REFRESH_TOKEN_KEY_PREFIX, oauthProvider.toLowerCase(), credentialId.toLowerCase());
}
+ private static String getAccessTokenKey(String oauthProvider, String credentialId) {
+ return String.format("%s-%s-%s", ACCESS_TOKEN_KEY_PREFIX, oauthProvider.toLowerCase(), credentialId.toLowerCase());
+ }
+
private static List> getKey(String name) {
List