/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 gobblin.runtime.locks; import java.io.IOException; import java.net.URI; import java.util.Properties; import java.util.Random; import java.util.concurrent.TimeoutException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsAction; import org.apache.hadoop.fs.permission.FsPermission; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import gobblin.configuration.ConfigurationKeys; import gobblin.runtime.api.JobSpec; import gobblin.util.HadoopUtils; /** * A factory for file-based job locks. All locks are presented as files under a common directory. * If the directory does not exist, it will be automatically created and removed on close(). */ public class FileBasedJobLockFactory implements JobLockFactory<FileBasedJobLock> { /** The URI of the file system with the directory for lock files*/ public static final String FS_URI_CONFIG = "fsURI"; /** The path to the directory for lock files*/ public static final String LOCK_DIR_CONFIG = "lockDir"; static final String DEFAULT_LOCK_DIR_PREFIX = "/tmp/gobblin-job-locks-"; /** * Default waiting period (5 minutes). * TODO add configuration support */ static final long DEFAULT_WAIT_MS = 300000; private final FileSystem fs; private final Path lockFileDir; private final Logger log; private final boolean deleteLockDirOnClose; /** Constructs a new factory * @throws IOException */ public FileBasedJobLockFactory(FileSystem fs, String lockFileDir, Optional<Logger> log) throws IOException { this.fs = fs; this.lockFileDir = new Path(lockFileDir); this.log = log.or(LoggerFactory.getLogger(getClass().getName() + "-" + lockFileDir)); this.deleteLockDirOnClose = !this.fs.exists(this.lockFileDir); if (deleteLockDirOnClose) { createLockDir(this.fs, this.lockFileDir); } } public FileBasedJobLockFactory(FileSystem fs, String lockFileDir) throws IOException { this(fs, lockFileDir, Optional.<Logger>absent()); } /** Create a new instance using the specified factory and hadoop configurations. */ public static FileBasedJobLockFactory create(Config factoryConfig, Configuration hadoopConf, Optional<Logger> log) throws IOException { FileSystem fs = factoryConfig.hasPath(FS_URI_CONFIG) ? FileSystem.get(URI.create(factoryConfig.getString(FS_URI_CONFIG)), hadoopConf) : getDefaultFileSystem(hadoopConf); String lockFilesDir = factoryConfig.hasPath(LOCK_DIR_CONFIG) ? factoryConfig.getString(LOCK_DIR_CONFIG) : getDefaultLockDir(fs, log); return new FileBasedJobLockFactory(fs, lockFilesDir, log); } public static FileSystem getDefaultFileSystem(Configuration hadoopConf) throws IOException { return FileSystem.getLocal(hadoopConf); } public static String getDefaultLockDir(FileSystem fs, Optional<Logger> log) { Random rng = new Random(); Path dirName; try { do { dirName = new Path(DEFAULT_LOCK_DIR_PREFIX + rng.nextLong()); } while (fs.exists(dirName)); } catch (IllegalArgumentException | IOException e) { throw new RuntimeException("Unable to create job lock directory: " + e, e); } if (log.isPresent()) { log.get().info("Created default job lock directory: " + dirName); } return dirName.toString(); } protected static void createLockDir(FileSystem fs, Path dirName) throws IOException { if (!fs.mkdirs(dirName, getDefaultDirPermissions())) { throw new RuntimeException("Unable to create job lock directory: " + dirName); } } protected static FsPermission getDefaultDirPermissions() { return new FsPermission(FsAction.ALL, FsAction.READ_EXECUTE, FsAction.NONE); } Path getLockFile(String jobName) { return new Path(lockFileDir, jobName + FileBasedJobLock.LOCK_FILE_EXTENSION); } /** * Acquire the lock. * * @throws JobLockException thrown if the {@link JobLock} fails to be acquired */ void lock(Path lockFile) throws JobLockException { log.debug("Creating lock: {}", lockFile); try { if (!this.fs.createNewFile(lockFile)) { throw new JobLockException("Failed to create lock file " + lockFile.getName()); } } catch (IOException e) { throw new JobLockException(e); } } /** * Release the lock. * * @throws JobLockException thrown if the {@link JobLock} fails to be released */ void unlock(Path lockFile) throws JobLockException { log.debug("Removing lock: {}", lockFile); if (!isLocked(lockFile)) { return; } try { this.fs.delete(lockFile, false); } catch (IOException e) { throw new JobLockException(e); } } /** * Try locking the lock. * * @return <em>true</em> if the lock is successfully locked, * <em>false</em> if otherwise. * @throws JobLockException thrown if the {@link JobLock} fails to be acquired */ boolean tryLock(Path lockFile) throws JobLockException { log.debug("Attempting lock: {}", lockFile); try { return this.fs.createNewFile(lockFile); } catch (IOException e) { throw new JobLockException(e); } } /** * Check if the lock is locked. * * @return if the lock is locked * @throws JobLockException thrown if checking the status of the {@link JobLock} fails */ boolean isLocked(Path lockFile) throws JobLockException { try { return this.fs.exists(lockFile); } catch (IOException e) { throw new JobLockException(e); } } public static Config getConfigForProperties(Properties properties) { return ConfigFactory.parseMap(ImmutableMap.<String, Object>builder() .put(FS_URI_CONFIG, properties.getProperty(ConfigurationKeys.FS_URI_KEY, ConfigurationKeys.LOCAL_FS_URI)) .put(LOCK_DIR_CONFIG, properties.getProperty(FileBasedJobLock.JOB_LOCK_DIR)) .build()); } public static FileBasedJobLockFactory createForProperties(Properties properties) throws JobLockException { try { FileSystem fs = FileSystem.get( URI.create(properties.getProperty(ConfigurationKeys.FS_URI_KEY, ConfigurationKeys.LOCAL_FS_URI)), HadoopUtils.getConfFromProperties(properties)); String lockFileDir = properties.getProperty(FileBasedJobLock.JOB_LOCK_DIR); return new FileBasedJobLockFactory(fs, lockFileDir); } catch (IOException e) { throw new JobLockException(e); } } @Override public FileBasedJobLock getJobLock(JobSpec jobSpec) throws TimeoutException { String jobName = getJobName(jobSpec); return new FileBasedJobLock(jobName, this); } @VisibleForTesting static String getJobName(JobSpec jobSpec) { return jobSpec.getUri().toString().replaceAll("[/.:]", "_"); } @VisibleForTesting FileSystem getFs() { return fs; } @VisibleForTesting Path getLockFileDir() { return lockFileDir; } @Override public void close() throws IOException { if (this.deleteLockDirOnClose) { this.log.info("Delete auto-created lock directory: {}", getLockFileDir()); if (!this.fs.delete(getLockFileDir(), true)) { this.log.warn("Failed to delete lock directory: {}", getLockFileDir()); } } } }