All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.jet.impl.JobClassLoaderService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2008-2024, Hazelcast, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hazelcast.jet.impl;

import com.hazelcast.core.HazelcastException;
import com.hazelcast.jet.JetException;
import com.hazelcast.jet.Util;
import com.hazelcast.jet.config.JetConfig;
import com.hazelcast.jet.config.JobConfig;
import com.hazelcast.jet.impl.deployment.ChildFirstClassLoader;
import com.hazelcast.jet.impl.deployment.JetClassLoader;
import com.hazelcast.jet.impl.deployment.JetDelegatingClassLoader;
import com.hazelcast.logging.ILogger;
import com.hazelcast.spi.impl.NodeEngine;
import com.hazelcast.spi.properties.ClusterProperty;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static com.hazelcast.jet.Util.idToString;
import static java.util.Collections.unmodifiableMap;

public class JobClassLoaderService {

    // The type of classLoaders field is CHM and not ConcurrentMap because we
    // rely on specific semantics of computeIfAbsent. ConcurrentMap.computeIfAbsent
    // does not guarantee at most one computation per key.
    // key: jobId
    private final ConcurrentHashMap classLoaders = new ConcurrentHashMap<>();

    private final ILogger logger;
    private final NodeEngine nodeEngine;
    private final JobRepository jobRepository;

    public JobClassLoaderService(@Nonnull NodeEngine nodeEngine, @Nonnull JobRepository jobRepository) {
        this.logger = nodeEngine.getLogger(getClass());
        this.nodeEngine = nodeEngine;
        this.jobRepository = jobRepository;
    }

    public Map getClassLoaders() {
        return Collections.unmodifiableMap(classLoaders);
    }

    /**
     * Get or create a Job classloader for a job with given config.
     * 

* It also creates processor classloaders if any are configured. * * @param config job config to use to create the classloader * @param jobId id of the job * @param phase phase for which the classloader is needed - coordinator/member * @return job classloader */ public ClassLoader getOrCreateClassLoader(JobConfig config, long jobId, JobPhase phase) { JetConfig jetConfig = nodeEngine.getConfig().getJetConfig(); JobClassLoaders jobClassLoaders = classLoaders.compute(jobId, (k, current) -> { JobClassLoaders result = current; if (current == null) { result = createJobClassLoaders(config, jobId, jetConfig); } result.recordPhase(phase); return result; } ); jobClassLoaders.recordPhase(phase); return jobClassLoaders.jobClassLoader(); } private JobClassLoaders createJobClassLoaders(JobConfig config, long jobId, JetConfig jetConfig) { return AccessController.doPrivileged( (PrivilegedAction) () -> { logger.fine("Creating job classLoader for job " + idToString(jobId)); ClassLoader parent = parentClassLoader(config); JetDelegatingClassLoader jobClassLoader; if (!jetConfig.isResourceUploadEnabled()) { jobClassLoader = new JetDelegatingClassLoader(parent); } else { jobClassLoader = new JetClassLoader(nodeEngine.getLogger(JetClassLoader.class), parent, config.getName(), jobId, jobRepository); } Map processorCls = createProcessorClassLoaders( jobId, config, jobClassLoader ); return new JobClassLoaders(jobClassLoader, processorCls); }); } private ClassLoader parentClassLoader(JobConfig config) { // config can be null for light jobs initialized after receiving a packet, but before the // InitExecutionOperation was received. We can ignore the classLoaderFactory, because // it's not supported anyway for light jobs. return config != null && config.getClassLoaderFactory() != null ? config.getClassLoaderFactory().getJobClassLoader() : nodeEngine.getConfigClassLoader(); } private Map createProcessorClassLoaders(long jobId, JobConfig jobConfig, ClassLoader parent) { logger.fine("Create processor classloader map for job " + idToString(jobId)); String customLibDir = nodeEngine.getProperties().getString(ClusterProperty.PROCESSOR_CUSTOM_LIB_DIR); Map classLoaderMap = new HashMap<>(); for (Entry> entry : jobConfig.getCustomClassPaths().entrySet()) { List list = entry.getValue().stream() .map(jar -> { try { assert Files.exists(Paths.get(customLibDir)) : "Directory " + customLibDir + " does not exist"; Path path = Paths.get(customLibDir, jar); return path.toUri().toURL(); } catch (MalformedURLException e) { throw new JetException(e); } }) .collect(Collectors.toList()); URL[] urls = list.toArray(new URL[]{}); classLoaderMap.put(entry.getKey(), new ChildFirstClassLoader(urls, parent)); } return unmodifiableMap(classLoaderMap); } /** * Prepare processor classloaders for given job for current thread * * @param jobId id of the job */ public void prepareProcessorClassLoaders(long jobId) { ProcessorClassLoaderTLHolder.putAll(getProcessorClassLoaders(jobId)); } private Map getProcessorClassLoaders(long jobId) { return classLoaders.get(jobId).processorCls(); } /** * Clears processor classloaders from the current thread */ public void clearProcessorClassLoaders() { ProcessorClassLoaderTLHolder.remove(); } /** * Returns classloader within which the initialization, execution and closing of * given processor should be done. *

* It returns processor classloader for a vertex with given name, in a job specified by the id; * however if there is no specific processor classloader, this method will return * {@link #getClassLoader(long) general job classloader}. *

* This method must be called after the classloader was created by * {@link #getOrCreateClassLoader(JobConfig, long, JobPhase)} on this * member. * * @param jobId job id * @param vertexName vertex name * @return processor classloader, null if the classloader is defined neither for the vertex, nor the job */ public ClassLoader getProcessorClassLoader(long jobId, String vertexName) { JobClassLoaders jobClassLoaders = classLoaders.get(jobId); if (jobClassLoaders != null) { ClassLoader classLoader = jobClassLoaders.processorCl(vertexName); return classLoader == null ? getClassLoader(jobId) : classLoader; } else { throw new HazelcastException("JobClassLoaders for jobId=" + Util.idToString(jobId) + " requested, but it does not exists"); } } /** * Try to remove and close/shutdown job classloader and any processor * classloaders for given job *

* We keep track of phases where the classloader is used and remove * the classloader only if there are no more phases left. */ public void tryRemoveClassloadersForJob(long jobId, JobPhase phase) { logger.finest("Try remove classloaders for jobId=%s, phase=%s", idToString(jobId), phase); classLoaders.compute(jobId, (k, jobClassLoaders) -> { if (jobClassLoaders == null) { logger.warning("JobClassLoaders for jobId=" + idToString(jobId) + " already removed"); return null; } int phaseCount = jobClassLoaders.removePhase(phase); if (phaseCount == 0) { logger.finest("JobClassLoaders phaseCount = 0, removing classloaders for jobId=%s", idToString(jobId)); Map processorCls = jobClassLoaders.processorCls(); if (processorCls != null) { for (ClassLoader cl : processorCls.values()) { try { ((ChildFirstClassLoader) cl).close(); } catch (IOException e) { logger.warning("Exception when closing processor classloader", e); } } } // the class loader might not have been initialized if the job failed before that JetDelegatingClassLoader jobClassLoader = jobClassLoaders.jobClassLoader(); jobClassLoader.shutdown(); logger.fine("Finish JobClassLoaders phaseCount = 0," + " removing classloaders for jobId=%s", idToString(jobId)); // Removes the item from the map return null; } else { logger.finest("JobClassLoaders refCount > 0, NOT removing classloaders for jobId=%s", idToString(jobId)); return jobClassLoaders; } }); } /** * Returns the job classloader for the job with given id. * * @param jobId job id * @return the job classloader, null if the classloader hasn't been created yet or was already destroyed */ public JetDelegatingClassLoader getClassLoader(long jobId) { JobClassLoaders jobClassLoaders = classLoaders.get(jobId); return jobClassLoaders == null ? null : jobClassLoaders.jobClassLoader; } /** * Phase where the classloader is needed */ public enum JobPhase { /** * Needed on job coordinator (master) for Pipeline/DAG deserialization and MetaSupplier init/get/close */ COORDINATOR, /** * Needed on member for ExecutionPlan deserialization, ProcessorSupplier init/get/close and Processors */ EXECUTION } /** * Keeps job classloader and potentially processor classloaders for a job. *

* Note: * On master node there is a race between closing PMS and PS. * We need to close the classloader only after both have been called. * When we create JobClassLoaders we record that by calling {@link #recordPhase(JobPhase)} and * when we release it we call {@link #removePhase(JobPhase)} */ private static class JobClassLoaders { private final JetDelegatingClassLoader jobClassLoader; private final Map processorCls; // EnumSet is not thread-safe // Interactions are synchronized on this. Its reference never leaks the JobClassLoaders instance. private final EnumSet phases = EnumSet.noneOf(JobPhase.class); JobClassLoaders( @Nonnull JetDelegatingClassLoader jobClassLoader, @Nonnull Map processorCls ) { this.jobClassLoader = jobClassLoader; this.processorCls = unmodifiableMap(processorCls); } public JetDelegatingClassLoader jobClassLoader() { return jobClassLoader; } public Map processorCls() { return processorCls; } public ClassLoader processorCl(String key) { return processorCls.get(key); } public void recordPhase(JobPhase phase) { synchronized (this) { phases.add(phase); } } public int removePhase(JobPhase phase) { synchronized (this) { phases.remove(phase); return phases.size(); } } @Override public String toString() { synchronized (this) { return "JobClassLoaders{" + "phases=" + phases + '}'; } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy