/** * Copyright 2015 Cloudera 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 org.kitesdk.data.spi.filesystem; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; 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.kitesdk.data.DatasetException; import org.kitesdk.data.DatasetIOException; import org.kitesdk.data.spi.Constraints; import org.kitesdk.data.Signalable; import com.google.common.base.Joiner; /** * Manager for creating, and checking {@link Signalable#isReady() ready} signals. * Stored in a filesystem, typically HDFS. */ public class SignalManager { private final Path signalDirectory; private final FileSystem rootFileSystem; private static final String UNBOUNDED_CONSTRAINT = "unbounded"; /** * Creates a new signal manager using the given signal directory. * * @param fileSystem a FileSystem that holds the signalDirectory * @param signalDirectory directory in which the manager * stores signals. * * @return a signal manager instance. */ public SignalManager(FileSystem fileSystem, Path signalDirectory) { this.signalDirectory = signalDirectory; this.rootFileSystem = fileSystem; } /** * Create a signal for the specified constraints. * * @param viewConstraints The constraints to create a signal for. * * @throws DatasetException if the signal could not be created. */ public void signalReady(Constraints viewConstraints) { try { rootFileSystem.mkdirs(signalDirectory); } catch (IOException e) { throw new DatasetIOException("Unable to create signal manager directory: " + signalDirectory, e); } String normalizedConstraints = getNormalizedConstraints(viewConstraints); Path signalPath = new Path(signalDirectory, normalizedConstraints); try{ // create the output stream to overwrite the current contents, if the directory or file // exists it will be overwritten to get a new timestamp FSDataOutputStream os = rootFileSystem.create(signalPath, true); os.close(); } catch (IOException e) { throw new DatasetIOException("Could not access signal path: " + signalPath, e); } } /** * Check the last time the specified constraints have been signaled as ready. * * @param viewConstraints The constraints to check for a signal. * * @return the timestamp of the last time the constraints were signaled as ready. * if the constraints have never been signaled, -1 will be returned. * * @throws DatasetException if the signals could not be accessed. */ public long getReadyTimestamp(Constraints viewConstraints) { String normalizedConstraints = getNormalizedConstraints(viewConstraints); Path signalPath = new Path(signalDirectory, normalizedConstraints); // check if the signal exists try { try { FileStatus signalStatus = rootFileSystem.getFileStatus(signalPath); return signalStatus.getModificationTime(); } catch (final FileNotFoundException ex) { // empty, will be thrown when the signal path doesn't exist } return -1; } catch (IOException e) { throw new DatasetIOException("Could not access signal path: " + signalPath, e); } } /** * Get a normalized query string for the {@link Constraints} that identifies a * logical {@code View}. * * The normalized constraints will match to the query portion of a URI that will * be exactly the same as another logically equivalent URI. * (where, for example, the query parameters may be re-ordered) * * If the constraints are {@link Constraints#isUnbounded() unbounded} a special case * of "unbounded" will be returned. * * @return a normalized query string for the specified constraints. * * @since 1.1 */ public static String getNormalizedConstraints(Constraints constraints) { // the constraints map isn't naturally ordered // we want to ensure that our output is if (constraints.isUnbounded()) { // unbounded constrains is a special case, here we just use // "unbounded" as the constraint return UNBOUNDED_CONSTRAINT; } Map<String, String> orderedConstraints = constraints.toNormalizedQueryMap(); List<String> parts = new ArrayList<String>(); // build a query portion of the URI for (Map.Entry<String, String> entry : orderedConstraints.entrySet()) { StringBuilder builder = new StringBuilder(); String key = entry.getKey(); String value = entry.getValue(); builder.append(key); builder.append("="); if (value != null) { builder.append(value); } parts.add(builder.toString()); } return Joiner.on('&').join(parts); } }