Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.cdap.cdap.internal.app.deploy.InMemoryProgramRunDispatcher Maven / Gradle / Ivy
/*
* 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.deploy;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.Closeables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import io.cdap.cdap.api.app.ApplicationSpecification;
import io.cdap.cdap.api.artifact.ApplicationClass;
import io.cdap.cdap.api.common.RuntimeArguments;
import io.cdap.cdap.api.plugin.Plugin;
import io.cdap.cdap.app.deploy.ConfigResponse;
import io.cdap.cdap.app.deploy.Configurator;
import io.cdap.cdap.app.deploy.ProgramRunDispatcher;
import io.cdap.cdap.app.deploy.ProgramRunDispatcherContext;
import io.cdap.cdap.app.guice.AppFabricServiceRuntimeModule;
import io.cdap.cdap.app.guice.ClusterMode;
import io.cdap.cdap.app.program.Program;
import io.cdap.cdap.app.program.ProgramDescriptor;
import io.cdap.cdap.app.program.Programs;
import io.cdap.cdap.app.runtime.ProgramController;
import io.cdap.cdap.app.runtime.ProgramOptions;
import io.cdap.cdap.app.runtime.ProgramRunner;
import io.cdap.cdap.app.runtime.ProgramRunnerFactory;
import io.cdap.cdap.common.ArtifactNotFoundException;
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.id.Id;
import io.cdap.cdap.common.internal.remote.NoOpInternalAuthenticator;
import io.cdap.cdap.common.internal.remote.RemoteClient;
import io.cdap.cdap.common.internal.remote.RemoteClientFactory;
import io.cdap.cdap.common.io.Locations;
import io.cdap.cdap.common.lang.jar.BundleJarUtil;
import io.cdap.cdap.common.lang.jar.ClassLoaderFolder;
import io.cdap.cdap.common.utils.DirUtils;
import io.cdap.cdap.common.utils.HashUtils;
import io.cdap.cdap.internal.app.deploy.pipeline.AppDeploymentInfo;
import io.cdap.cdap.internal.app.deploy.pipeline.AppDeploymentRuntimeInfo;
import io.cdap.cdap.internal.app.deploy.pipeline.AppSpecInfo;
import io.cdap.cdap.internal.app.runtime.BasicArguments;
import io.cdap.cdap.internal.app.runtime.ProgramOptionConstants;
import io.cdap.cdap.internal.app.runtime.ProgramRunners;
import io.cdap.cdap.internal.app.runtime.SimpleProgramOptions;
import io.cdap.cdap.internal.app.runtime.SystemArguments;
import io.cdap.cdap.internal.app.runtime.artifact.ArtifactDescriptor;
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.artifact.PluginFinder;
import io.cdap.cdap.internal.app.runtime.artifact.RemoteArtifactRepository;
import io.cdap.cdap.internal.app.runtime.artifact.RemoteArtifactRepositoryReader;
import io.cdap.cdap.internal.app.runtime.artifact.RemoteIsolatedPluginFinder;
import io.cdap.cdap.internal.tethering.NoOpDiscoveryServiceClient;
import io.cdap.cdap.internal.tethering.TetheringAgentService;
import io.cdap.cdap.proto.id.ArtifactId;
import io.cdap.cdap.proto.id.ProgramId;
import io.cdap.cdap.security.impersonation.Impersonator;
import io.cdap.cdap.security.spi.authenticator.RemoteAuthenticator;
import io.cdap.common.http.HttpRequestConfig;
import org.apache.twill.api.RunId;
import org.apache.twill.filesystem.Location;
import org.apache.twill.filesystem.LocationFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URI;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
/**
* In-memory Implementation for {@link ProgramRunDispatcher} which runs the program in the same service from which it's
* invoked.
*/
public class InMemoryProgramRunDispatcher implements ProgramRunDispatcher {
private static final String CLUSTER_SCOPE = "cluster";
private static final String APPLICATION_SCOPE = "app";
private static final Logger LOG = LoggerFactory.getLogger(InMemoryProgramRunDispatcher.class);
private final CConfiguration cConf;
private final Impersonator impersonator;
private final ProgramRunnerFactory programRunnerFactory;
private final LocationFactory locationFactory;
private final RemoteClientFactory remoteClientFactory;
private final PluginFinder pluginFinder;
private final ArtifactRepository noAuthArtifactRepository;
private final boolean artifactsComputeHash;
private final int artifactsComputeHashTimeBucketDays;
private final boolean artifactsComputeHashSnapshot;
private RemoteAuthenticator remoteAuthenticator;
private ProgramRunnerFactory remoteProgramRunnerFactory;
private String hostname;
@Inject
public InMemoryProgramRunDispatcher(CConfiguration cConf, ProgramRunnerFactory programRunnerFactory,
Impersonator impersonator,
LocationFactory locationFactory, RemoteClientFactory remoteClientFactory,
@Named(AppFabricServiceRuntimeModule.NOAUTH_ARTIFACT_REPO)
ArtifactRepository artifactRepository,
PluginFinder pluginFinder) {
this.cConf = cConf;
this.programRunnerFactory = programRunnerFactory;
this.impersonator = impersonator;
this.locationFactory = locationFactory;
this.remoteClientFactory = remoteClientFactory;
this.noAuthArtifactRepository = artifactRepository;
this.pluginFinder = pluginFinder;
this.artifactsComputeHash = cConf.getBoolean(Constants.AppFabric.ARTIFACTS_COMPUTE_HASH);
this.artifactsComputeHashSnapshot = cConf.getBoolean(Constants.AppFabric.ARTIFACTS_COMPUTE_HASH_SNAPSHOT);
this.artifactsComputeHashTimeBucketDays = cConf.getInt(Constants.AppFabric.ARTIFACTS_COMPUTE_HASH_TIME_BUCKET_DAYS);
}
/**
* Optional guice injection for the {@link ProgramRunnerFactory} used for remote execution. It is optional because in
* unit-test we don't have need for that.
*/
@Inject(optional = true)
public void setRemoteProgramRunnerFactory(@Constants.AppFabric.RemoteExecution ProgramRunnerFactory runnerFactory) {
this.remoteProgramRunnerFactory = runnerFactory;
}
@Inject(optional = true)
public void setHostname(@Named(Constants.Service.MASTER_SERVICES_BIND_ADDRESS) InetAddress host) {
if (Objects.nonNull(host)) {
this.hostname = host.getCanonicalHostName();
}
}
@Inject(optional = true)
public void setRemoteAuthenticator(@Named(TetheringAgentService.REMOTE_TETHERING_AUTHENTICATOR)
RemoteAuthenticator remoteAuthenticator) {
this.remoteAuthenticator = remoteAuthenticator;
}
@Override
public ProgramController dispatchProgram(ProgramRunDispatcherContext dispatcherContext) throws Exception {
RunId runId = dispatcherContext.getRunId();
LOG.debug("Preparing to dispatch program run: {}", runId);
ProgramDescriptor programDescriptor = dispatcherContext.getProgramDescriptor();
ProgramOptions options = dispatcherContext.getProgramOptions();
boolean isDistributed = dispatcherContext.isDistributed();
ProgramId programId = programDescriptor.getProgramId();
ClusterMode clusterMode = ProgramRunners.getClusterMode(options);
boolean tetheredRun = options.getArguments().hasOption(ProgramOptionConstants.PEER_NAME);
ProgramRunnerFactory progRunnerFactory = programRunnerFactory;
if (clusterMode == ClusterMode.ISOLATED && !tetheredRun) {
progRunnerFactory = Optional.ofNullable(remoteProgramRunnerFactory)
.orElseThrow(UnsupportedOperationException::new);
}
ArtifactRepository artifactRepository = noAuthArtifactRepository;
String peer = options.getArguments().getOption(ProgramOptionConstants.PEER_NAME);
if (peer != null) {
// For tethered pipeline runs, fetch artifacts from ArtifactCacheService
String basePath = String.format("%s/peers/%s", Constants.Gateway.INTERNAL_API_VERSION_3, peer);
// Set longer timeouts because we fetch from remote appfabric the first time we get an artifact. Subsequent
// reads are served by the cache.
HttpRequestConfig requestConfig = new HttpRequestConfig(600000,
600000,
false);
RemoteClient client = remoteClientFactory.createRemoteClient(Constants.Service.ARTIFACT_CACHE_SERVICE,
requestConfig,
basePath);
RemoteArtifactRepositoryReader artifactRepositoryReader = new RemoteArtifactRepositoryReader(locationFactory,
client);
artifactRepository = new RemoteArtifactRepository(cConf, artifactRepositoryReader);
}
// Creates the ProgramRunner based on the cluster mode
ProgramRunner runner = progRunnerFactory.create(programId.getType());
dispatcherContext.addCleanupTask(createCleanupTask(runner));
File tempDir = createTempDirectory(programId, runId);
dispatcherContext.addCleanupTask(createCleanupTask(tempDir));
// Get the artifact details and save it into the program options.
ArtifactId artifactId = programDescriptor.getArtifactId();
ArtifactDetail artifactDetail = getArtifactDetail(artifactId, artifactRepository);
ApplicationSpecification appSpec = programDescriptor.getApplicationSpecification();
ProgramDescriptor newProgramDescriptor = programDescriptor;
ProgramOptions updatedOptions = options;
if (tetheredRun) {
updatedOptions = updateProgramOptions(artifactId, programId, options, runId, clusterMode,
Iterables.getFirst(artifactDetail.getMeta().getClasses().getApps(),
null),
isDistributed);
// Take a snapshot of all the plugin artifacts used by the program
updatedOptions = createPluginSnapshot(updatedOptions, programId, tempDir,
newProgramDescriptor.getApplicationSpecification(),
isDistributed, artifactRepository);
// Download the program jar into the target directory
// This hack is here because the program jar won't actually exist in a tethered run
// TODO: (CDAP-19150) remove hack once Location is removed form ArtifactDetail
Id.Namespace namespace = Id.Namespace.from(programDescriptor.getProgramId().getNamespace());
Id.Artifact aId = Id.Artifact.from(namespace, artifactDetail.getDescriptor().getArtifactId());
File targetFile = new File(tempDir, "program.jar");
Path target = targetFile.toPath();
downloadArtifact(aId, target, artifactRepository);
Location location = Locations.toLocation(target);
ArtifactDescriptor descriptor = new ArtifactDescriptor(artifactDetail.getDescriptor().getNamespace(),
artifactDetail.getDescriptor().getArtifactId(),
location);
artifactDetail = new ArtifactDetail(descriptor, artifactDetail.getMeta());
}
boolean isPreview =
Boolean.parseBoolean(options.getArguments().getOption(ProgramOptionConstants.IS_PREVIEW, "false"));
// Do the app spec regeneration if the mode is on premise or if it's a tethered run.
// For non-tethered run in isolated mode, the regeneration is done on the runtime environment before the program
// launch. For preview we already have a resolved app spec, so no need to regenerate the app spec again
if (!isPreview && appSpec != null &&
(ClusterMode.ON_PREMISE.equals(clusterMode) || tetheredRun)) {
RemoteClientFactory factory = remoteClientFactory;
PluginFinder pf = pluginFinder;
if (tetheredRun) {
// Configure remote client to talk to the specified tethered peer endpoint.
// For example, if the peer endpoint is https://my.host.com/api and a remote client is created with
// basePath = /v3, then some/path should resolve to https://my.host.com/api/v3/some/path.
String peerEndpoint = options.getArguments().getOption(ProgramOptionConstants.PEER_ENDPOINT);
String pathPrefix = new URI(peerEndpoint).getPath();
factory = new RemoteClientFactory(new NoOpDiscoveryServiceClient(peerEndpoint),
new NoOpInternalAuthenticator(),
remoteAuthenticator, pathPrefix);
pf = new RemoteIsolatedPluginFinder(locationFactory, factory,
tempDir.getAbsolutePath());
}
try {
ApplicationSpecification generatedAppSpec =
regenerateAppSpec(artifactDetail, programId, artifactId, appSpec, updatedOptions, pf, factory,
artifactRepository);
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", programId, e);
}
}
if (!tetheredRun) {
ArtifactDescriptor artifactDescriptor = artifactDetail.getDescriptor();
if (artifactsComputeHash && (artifactsComputeHashSnapshot ||
!artifactDescriptor.getArtifactId().getVersion().isSnapshot())) {
Hasher hasher = Hashing.sha256().newHasher();
hasher.putString(artifactDescriptor.getNamespace());
hasher.putString(artifactDescriptor.getArtifactId().getName());
hasher.putString(artifactDescriptor.getArtifactId().getScope().name());
hasher.putString(artifactDescriptor.getArtifactId().getVersion().getVersion());
Map arguments = new HashMap<>(options.getArguments().asMap());
arguments.put(ProgramOptionConstants.PROGRAM_JAR_HASH, getArtifactHash(hasher));
options = new SimpleProgramOptions(options.getProgramId(), new BasicArguments(arguments),
new BasicArguments(options.getUserArguments().asMap()), options.isDebug());
}
updatedOptions = updateProgramOptions(artifactId, programId, options, runId, clusterMode,
Iterables.getFirst(artifactDetail.getMeta().getClasses().getApps(),
null),
isDistributed);
// Take a snapshot of plugin artifacts. In the tethered case, we did this before regenerating app spec because
// PLUGIN_DIR program option is set in createPluginSnapshot(). This option is read while regenerating app spec.
updatedOptions = createPluginSnapshot(updatedOptions, programId, tempDir,
newProgramDescriptor.getApplicationSpecification(),
isDistributed, artifactRepository);
}
// Create and run the program
Program executableProgram = createProgram(cConf, runner, newProgramDescriptor, artifactDetail,
tempDir, tetheredRun);
dispatcherContext.addCleanupTask(createCleanupTask(executableProgram));
return runner.run(executableProgram, updatedOptions);
}
/**
* Regenerates the app spec before the program start
* TODO(CDAP-19275): Consolidate appspec regeneration logic - on_premise, tethered and runs on Dataproc.
* @return the regenerated app spec, or null if there is any exception generating the app spec.
*/
@Nullable
protected ApplicationSpecification regenerateAppSpec(ArtifactDetail artifactDetail, ProgramId programId,
ArtifactId artifactId, ApplicationSpecification existingAppSpec,
ProgramOptions options, PluginFinder pluginFinder,
RemoteClientFactory factory,
ArtifactRepository artifactRepository)
throws InterruptedException, ExecutionException, TimeoutException {
ApplicationClass appClass = Iterables.getFirst(artifactDetail.getMeta().getClasses().getApps(), null);
if (appClass == null) {
// This should never happen.
throw new IllegalStateException(
String.format("No application class found in artifact '%s' in namespace '%s'.",
artifactDetail.getDescriptor().getArtifactId(), programId.getNamespace()));
}
Map runtimeArguments =
SystemArguments.skipNormalMacroEvaluation(options.getUserArguments().asMap())
? Collections.emptyMap() : options.getUserArguments().asMap();
AppDeploymentInfo deploymentInfo = AppDeploymentInfo.builder()
.setArtifactId(artifactId)
.setArtifactLocation(artifactDetail.getDescriptor().getLocation())
.setNamespaceId(programId.getNamespaceId())
.setApplicationClass(appClass)
.setAppName(existingAppSpec.getName())
.setAppVersion(existingAppSpec.getAppVersion())
.setConfigString(existingAppSpec.getConfiguration())
.setUpdateSchedules(false)
.setRuntimeInfo(new AppDeploymentRuntimeInfo(existingAppSpec,
runtimeArguments, options.getArguments().asMap()))
.setDeployedApplicationSpec(existingAppSpec)
.build();
Configurator configurator = new InMemoryConfigurator(cConf, pluginFinder, impersonator,
artifactRepository, factory, deploymentInfo);
ListenableFuture future = configurator.config();
ConfigResponse response = future.get(120, TimeUnit.SECONDS);
if (response.getExitCode() == 0) {
AppSpecInfo appSpecInfo = response.getAppSpecInfo();
if (appSpecInfo != null && appSpecInfo.getAppSpec() != null) {
return appSpecInfo.getAppSpec();
}
}
return null;
}
/**
* Downloads the artifact to the given path.
*/
protected void downloadArtifact(Id.Artifact artifactId, Path target, ArtifactRepository artifactRepository)
throws NotFoundException, IOException {
try (InputStream is = artifactRepository.newInputStream(artifactId)) {
Files.copy(is, target);
}
}
/**
* Creates a {@link Program} for the given {@link ProgramRunner} from the given program jar {@link Location}.
*/
protected Program createProgram(CConfiguration cConf, ProgramRunner programRunner,
ProgramDescriptor programDescriptor, ArtifactDetail artifactDetail,
File tempDir, boolean isTetheredRun) throws IOException {
// TODO: (CDAP-19150) remove Location usage from ArtifactDetail
Location programJarLocation = artifactDetail.getDescriptor().getLocation();
try {
// If the program jar is not a directory, take a snapshot of the jar file to avoid mutation.
if (!programJarLocation.isDirectory()) {
File targetFile = new File(tempDir, "program.jar");
if (isTetheredRun) {
// This hack is here because the program jar won't actually exist in a tethered run
// TODO: (CDAP-19150) remove hack once Location is removed form ArtifactDetail
programJarLocation = Locations.toLocation(targetFile);
} else {
try {
programJarLocation = Locations.toLocation(Locations.linkOrCopyOverwrite(programJarLocation, targetFile));
} catch (FileAlreadyExistsException ex) {
LOG.warn("Program file {} already exists and can not be replaced.", targetFile.getAbsolutePath());
}
}
}
// Unpack the JAR file
ClassLoaderFolder classLoaderFolder = BundleJarUtil.prepareClassLoaderFolder(
programJarLocation, () -> Files.createTempDirectory(tempDir.toPath(), "unpacked").toFile());
return Programs.create(cConf, programRunner, programDescriptor, programJarLocation, classLoaderFolder.getDir());
} catch (IOException ioe) {
throw ioe;
} catch (Exception e) {
// should not happen
throw Throwables.propagate(e);
}
}
protected ArtifactDetail getArtifactDetail(ArtifactId artifactId, ArtifactRepository artifactRepository)
throws Exception {
return artifactRepository.getArtifact(Id.Artifact.fromEntityId(artifactId));
}
/**
* Creates a local temporary directory for this program run.
*/
private File createTempDirectory(ProgramId programId, RunId runId) {
File tempDir =
new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR), cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
File dir = new File(tempDir,
String.format("%s.%s.%s.%s.%s", programId.getType().name().toLowerCase(),
programId.getNamespace(), programId.getApplication(), programId.getProgram(),
runId.getId()));
dir.mkdirs();
return dir;
}
private Runnable createCleanupTask(final Object... resources) {
AtomicBoolean executed = new AtomicBoolean();
return () -> {
if (!executed.compareAndSet(false, true)) {
return;
}
List resourceList = new ArrayList<>(Arrays.asList(resources));
Collections.reverse(resourceList);
for (Object resource : resourceList) {
if (resource == null) {
continue;
}
try {
if (resource instanceof File) {
File file = (File) resource;
if (file.isDirectory()) {
DirUtils.deleteDirectoryContents(file);
} else {
file.delete();
}
} else if (resource instanceof Closeable) {
Closeables.closeQuietly((Closeable) resource);
} else if (resource instanceof Runnable) {
((Runnable) resource).run();
}
} catch (Throwable t) {
LOG.warn("Exception when cleaning up resource {}", resource, t);
}
}
};
}
/**
* Updates the given {@link ProgramOptions} and return a new instance. It copies the {@link ProgramOptions}. Then it
* adds all entries returned by {@link #getExtraProgramOptions(boolean)} followed by adding the {@link RunId} to the
* system arguments.
*
* Also scope resolution will be performed on the user arguments on the application and program.
*
* @param programId the program id
* @param options The {@link ProgramOptions} in which the RunId to be included
* @param runId The RunId to be included
* @param clusterMode clustermode for the program run
* @param applicationClass application class for the program
* @return the copy of the program options with RunId included in them
*/
private ProgramOptions updateProgramOptions(ArtifactId artifactId, ProgramId programId, ProgramOptions options,
RunId runId, ClusterMode clusterMode, ApplicationClass applicationClass,
boolean isDistributed) {
// Build the system arguments
Map systemArguments = new HashMap<>(options.getArguments().asMap());
// don't add these system arguments if they're already there
// this can happen if this is a program within a workflow, and the workflow already added these arguments
Map extraProgramOptions = getExtraProgramOptions(isDistributed);
for (Map.Entry extraOption : extraProgramOptions.entrySet()) {
systemArguments.putIfAbsent(extraOption.getKey(), extraOption.getValue());
}
systemArguments.putIfAbsent(ProgramOptionConstants.RUN_ID, runId.getId());
systemArguments.putIfAbsent(ProgramOptionConstants.ARTIFACT_ID, Joiner.on(':').join(artifactId.toIdParts()));
if (clusterMode == ClusterMode.ISOLATED) {
systemArguments.putIfAbsent(ProgramOptionConstants.APPLICATION_CLASS, applicationClass.getClassName());
}
// Resolves the user arguments
// First resolves at the cluster scope if the cluster.name is not empty
String clusterName = options.getArguments().getOption(Constants.CLUSTER_NAME);
Map userArguments = options.getUserArguments().asMap();
if (!Strings.isNullOrEmpty(clusterName)) {
userArguments = RuntimeArguments.extractScope(CLUSTER_SCOPE, clusterName, userArguments);
}
// Then resolves at the application scope
userArguments = RuntimeArguments.extractScope(APPLICATION_SCOPE, programId.getApplication(), userArguments);
// Then resolves at the program level
userArguments =
RuntimeArguments.extractScope(programId.getType().getScope(), programId.getProgram(), userArguments);
return new SimpleProgramOptions(options.getProgramId(), new BasicArguments(systemArguments),
new BasicArguments(userArguments), options.isDebug());
}
private static void hashArtifactId(Hasher hasher, ArtifactId artifactId) {
hasher.putString(artifactId.getParent().toString());
hasher.putString(artifactId.getArtifact());
hasher.putString(artifactId.getVersion());
}
/**
* Return the copy of the {@link ProgramOptions} including locations of plugin artifacts in it.
*
* @param options the {@link ProgramOptions} in which the locations of plugin artifacts needs to be included
* @param programId Id of the Program
* @param tempDir Temporary Directory to create the plugin artifact snapshot
* @param appSpec program's Application Specification
* @return the copy of the program options with locations of plugin artifacts included in them
*/
private ProgramOptions createPluginSnapshot(ProgramOptions options, ProgramId programId, File tempDir,
@Nullable ApplicationSpecification appSpec,
boolean isDistributed, ArtifactRepository artifactRepository)
throws Exception {
// appSpec is null in an unit test
if (appSpec == null) {
return options;
}
Set files = Sets.newHashSet();
HashMap arguments = new HashMap<>(options.getArguments().asMap());
Hasher hasher = Hashing.sha256().newHasher();
/**
* If there is an artifact with SNAPSHOT version, hash should not be computed because artifact with
* SNAPSHOT might get changed but hash value remaining the same.
* {@link Constants.AppFabric.ARTIFACTS_COMPUTE_HASH_SNAPSHOT} allows to compute hash on snapshots
* which should only be used in testings.
*/
boolean computeHash = artifactsComputeHash && !appSpec.getPlugins().isEmpty() &&
(artifactsComputeHashSnapshot ||
appSpec.getPlugins().values().stream().allMatch(plugin -> !plugin.getArtifactId().getVersion().isSnapshot()));
// Sort plugins based on keys so generated hashes remain identical
SortedMap sortedMap = new TreeMap<>(appSpec.getPlugins());
for (Map.Entry entry : sortedMap.entrySet()) {
Plugin plugin = entry.getValue();
File destFile = new File(tempDir, Artifacts.getFileName(plugin.getArtifactId()));
// Skip if the file has already been copied.
if (!files.add(destFile.getName())) {
continue;
}
try {
ArtifactId artifactId = Artifacts.toProtoArtifactId(programId.getNamespaceId(), plugin.getArtifactId());
if (computeHash) {
hashArtifactId(hasher, artifactId);
}
String peer = options.getArguments().getOption(ProgramOptionConstants.PEER_NAME);
ArtifactDetail artifactDetail = getArtifactDetail(artifactId, artifactRepository);
copyArtifact(artifactId, artifactDetail, destFile, artifactRepository, isDistributed, peer != null);
} catch (ArtifactNotFoundException e) {
throw new IllegalArgumentException(String.format("Artifact %s could not be found", plugin.getArtifactId()), e);
}
}
LOG.debug("Plugin artifacts of {} copied to {}", programId, tempDir.getAbsolutePath());
arguments.put(ProgramOptionConstants.PLUGIN_DIR, tempDir.getAbsolutePath());
if (computeHash) {
arguments.put(ProgramOptionConstants.PLUGIN_DIR_HASH, getArtifactHash(hasher));
}
return new SimpleProgramOptions(options.getProgramId(), new BasicArguments(ImmutableMap.copyOf(arguments)),
options.getUserArguments(), options.isDebug());
}
/**
* Copies the artifact jar to the given target file. Copies artifact from ArtifactCacheService if the program run is
* for a tethered peer, else copies the artifact from the filesystem.
*
* @param artifactId artifact id of the artifact to be copied
* @param artifactDetail detail information of the artifact to be copied
* @param targetFile target file to copy to
* @throws IOException if the copying failed
*/
private void copyArtifact(ArtifactId artifactId, ArtifactDetail artifactDetail, File targetFile,
ArtifactRepository artifactRepository, boolean isDistributed, boolean isTetheredPeer)
throws Exception {
if (isTetheredPeer) {
try (InputStream in = artifactRepository.newInputStream(Id.Artifact.fromEntityId(artifactId))) {
copyArtifact(artifactId, targetFile, isDistributed, () -> {
Files.copy(in, targetFile.toPath());
return null;
});
}
} else {
copyArtifact(artifactId, targetFile, isDistributed, () -> {
Locations.linkOrCopy(artifactDetail.getDescriptor().getLocation(), targetFile);
return null;
});
}
}
private void copyArtifact(ArtifactId artifactId, File targetFile, boolean isDistributed, Callable operation)
throws Exception {
if (isDistributed) {
try {
impersonator.doAs(artifactId, operation);
} catch (FileAlreadyExistsException ex) {
LOG.warn("Artifact file {} already exists.", targetFile.getAbsolutePath());
} catch (Exception e) {
Throwables.propagateIfPossible(e, IOException.class);
// should not happen
throw Throwables.propagate(e);
}
} else {
operation.call();
}
}
private Map getExtraProgramOptions(boolean isDistributed) {
return !isDistributed && Objects.nonNull(hostname) ? Collections.singletonMap(ProgramOptionConstants.HOST, hostname)
: Collections.emptyMap();
}
private String getArtifactHash(Hasher hasher) {
String hashVal = hasher.hash().toString();
if (artifactsComputeHashTimeBucketDays > 0) {
return HashUtils.timeBucketHash(hashVal, artifactsComputeHashTimeBucketDays, System.currentTimeMillis());
}
return hashVal;
}
}