/* * 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.spec_store; import java.io.IOException; import java.net.URI; import java.util.Collection; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.typesafe.config.Config; import gobblin.configuration.ConfigurationKeys; import gobblin.runtime.api.GobblinInstanceEnvironment; import gobblin.runtime.api.Spec; import gobblin.runtime.api.SpecNotFoundException; import gobblin.runtime.api.SpecSerDe; import gobblin.runtime.api.SpecStore; import gobblin.util.PathUtils; /** * The Spec Store for file system to persist the Spec information. * Note: * 1. This implementation has no support for caching. * 2. This implementation does not performs implicit version management. * For implicit version management, please use a wrapper FSSpecStore. */ public class FSSpecStore implements SpecStore { protected final Logger log; protected final Config sysConfig; protected final FileSystem fs; protected final String fsSpecStoreDir; protected final Path fsSpecStoreDirPath; protected final SpecSerDe specSerDe; public FSSpecStore(GobblinInstanceEnvironment env, SpecSerDe specSerDe) throws IOException { this(env.getSysConfig().getConfig(), specSerDe, Optional.<Logger>absent()); } public FSSpecStore(Config sysConfig, SpecSerDe specSerDe) throws IOException { this(sysConfig, specSerDe, Optional.<Logger>absent()); } public FSSpecStore(GobblinInstanceEnvironment env, SpecSerDe specSerDe, Optional<Logger> log) throws IOException { this(env.getSysConfig().getConfig(), specSerDe, log); } public FSSpecStore(Config sysConfig, SpecSerDe specSerDe, Optional<Logger> log) throws IOException { Preconditions.checkArgument(sysConfig.hasPath(ConfigurationKeys.SPECSTORE_FS_DIR_KEY), "FS SpecStore path must be specified."); this.log = log.isPresent() ? log.get() : LoggerFactory.getLogger(getClass()); this.sysConfig = sysConfig; this.specSerDe = specSerDe; this.fsSpecStoreDir = this.sysConfig.getString(ConfigurationKeys.SPECSTORE_FS_DIR_KEY); this.fsSpecStoreDirPath = new Path(this.fsSpecStoreDir); this.log.info("FSSpecStore directory is: " + this.fsSpecStoreDir); try { this.fs = this.fsSpecStoreDirPath.getFileSystem(new Configuration()); } catch (IOException e) { throw new RuntimeException("Unable to detect job config directory file system: " + e, e); } if (!this.fs.exists(this.fsSpecStoreDirPath)) { this.log.info("FSSpecStore directory: " + this.fsSpecStoreDir + " did not exist. Creating it."); this.fs.mkdirs(this.fsSpecStoreDirPath); } } @Override public boolean exists(URI specUri) throws IOException { Preconditions.checkArgument(null != specUri, "Spec URI should not be null"); FileStatus[] fileStatuses = fs.listStatus(this.fsSpecStoreDirPath); for (FileStatus fileStatus : fileStatuses) { if (StringUtils.startsWith(fileStatus.getPath().getName(), specUri.toString())) { return true; } } return false; } @Override public void addSpec(Spec spec) throws IOException { Preconditions.checkArgument(null != spec, "Spec should not be null"); log.info(String.format("Adding Spec with URI: %s in FSSpecStore: %s", spec.getUri(), this.fsSpecStoreDirPath)); Path specPath = getPathForURI(this.fsSpecStoreDirPath, spec.getUri(), spec.getVersion()); writeSpecToFile(specPath, spec); } @Override public boolean deleteSpec(Spec spec) throws IOException { Preconditions.checkArgument(null != spec, "Spec should not be null"); return deleteSpec(spec.getUri(), spec.getVersion()); } @Override public boolean deleteSpec(URI specUri) throws IOException { Preconditions.checkArgument(null != specUri, "Spec URI should not be null"); try { return deleteSpec(specUri, getSpec(specUri).getVersion()); } catch (SpecNotFoundException e) { throw new IOException(String.format("Issue in removing Spec: %s", specUri), e); } } @Override public boolean deleteSpec(URI specUri, String version) throws IOException { Preconditions.checkArgument(null != specUri, "Spec URI should not be null"); Preconditions.checkArgument(null != version, "Version should not be null"); try { log.info(String.format("Deleting Spec with URI: %s in FSSpecStore: %s", specUri, this.fsSpecStoreDirPath)); Path specPath = getPathForURI(this.fsSpecStoreDirPath, specUri, version); if (fs.exists(specPath)) { return fs.delete(specPath, false); } else { log.warn("No file with URI:" + specUri + " is found. Deletion failed."); return false; } } catch (IOException e) { throw new IOException(String.format("Issue in removing Spec: %s for Version: %s", specUri, version), e); } } @Override public Spec updateSpec(Spec spec) throws IOException, SpecNotFoundException { Preconditions.checkArgument(null != spec, "Spec should not be null"); log.info(String.format("Updating Spec with URI: %s in FSSpecStore: %s", spec.getUri(), this.fsSpecStoreDirPath)); Path specPath = getPathForURI(this.fsSpecStoreDirPath, spec.getUri(), spec.getVersion()); writeSpecToFile(specPath, spec); return spec; } @Override public Spec getSpec(URI specUri) throws IOException, SpecNotFoundException { Preconditions.checkArgument(null != specUri, "Spec URI should not be null"); Collection<Spec> specs = getAllVersionsOfSpec(specUri); Spec highestVersionSpec = null; for (Spec spec : specs) { if (null == highestVersionSpec) { highestVersionSpec = spec; } else if (null != spec.getVersion() && spec.getVersion().compareTo(spec.getVersion()) > 0) { highestVersionSpec = spec; } } if (null == highestVersionSpec) { throw new SpecNotFoundException(specUri); } return highestVersionSpec; } @Override public Spec getSpec(URI specUri, String version) throws IOException, SpecNotFoundException { Preconditions.checkArgument(null != specUri, "Spec URI should not be null"); Preconditions.checkArgument(null != version, "Version should not be null"); Path specPath = getPathForURI(this.fsSpecStoreDirPath, specUri, version); if (!fs.exists(specPath)) { throw new SpecNotFoundException(specUri); } return readSpecFromFile(specPath); } @Override public Collection<Spec> getAllVersionsOfSpec(URI specUri) throws IOException, SpecNotFoundException { Preconditions.checkArgument(null != specUri, "Spec URI should not be null"); Collection<Spec> specs = getSpecs(); Collection<Spec> filteredSpecs = Lists.newArrayList(); for (Spec spec : specs) { if (spec.getUri().equals(specUri)) { filteredSpecs.add(spec); } } if (filteredSpecs.size() == 0) { throw new SpecNotFoundException(specUri); } return filteredSpecs; } @Override public Collection<Spec> getSpecs() throws IOException { Collection<Spec> specs = Lists.newArrayList(); try { getSpecs(this.fsSpecStoreDirPath, specs); } catch (Exception e) { throw new IOException(e); } return specs; } private void getSpecs(Path directory, Collection<Spec> specs) throws IOException { FileStatus[] fileStatuses = fs.listStatus(directory); for (FileStatus fileStatus : fileStatuses) { if (fileStatus.isDirectory()) { getSpecs(fileStatus.getPath(), specs); } else { specs.add(readSpecFromFile(fileStatus.getPath())); } } } /*** * Read and deserialized Spec from a file. * @param path File containing serialized Spec. * @return Spec * @throws IOException */ protected Spec readSpecFromFile(Path path) throws IOException { Spec spec = null; try (FSDataInputStream fis = fs.open(path);) { spec = this.specSerDe.deserialize(IOUtils.toByteArray(fis)); } return spec; } /*** * Serialize and write Spec to a file. * @param specPath Spec file name. * @param spec Spec object to write. * @throws IOException */ protected void writeSpecToFile(Path specPath, Spec spec) throws IOException { if (fs.exists(specPath)) { fs.delete(specPath, true); } byte[] serializedSpec = this.specSerDe.serialize(spec); try (FSDataOutputStream os = fs.create(specPath)) { os.write(serializedSpec); } } /** * * @param fsSpecStoreDirPath The directory path for specs. * @param uri Uri as the identifier of JobSpec * @return */ protected Path getPathForURI(Path fsSpecStoreDirPath, URI uri, String version) { return PathUtils.addExtension(PathUtils.mergePaths(fsSpecStoreDirPath, new Path(uri)), version); } }