/*
* ProActive Parallel Suite(TM):
* The Open Source library for parallel and distributed
* Workflows & Scheduling, Orchestration, Cloud Automation
* and Big Data Analysis on Enterprise Grids & Clouds.
*
* Copyright (c) 2007 - 2017 ActiveEon
* Contact: contact@activeeon.com
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation: version 3 of
* the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* If needed, contact us to obtain a release under GPL Version 2 or 3
* or a different license than the AGPL.
*/
package org.ow2.proactive.scheduler.task;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.apache.log4j.Logger;
import org.ow2.proactive.scheduler.common.task.TaskId;
import org.ow2.proactive.scheduler.task.utils.ForkerUtils;
/**
* ProgressFileReader is in charge of:
* - creating a progress file in the specified working directory
* - detecting changes on a progress file efficiently
* - reading new value and saving it in memory
* - exposing the last read value
* <p>
* Instances of this class are NOT thread-safe.
*
* @author The ProActive Team
*/
public class ProgressFileReader {
private static final Logger logger = Logger.getLogger(ProgressFileReader.class);
private static final String PROGRESS_FILE_DIR = ".tasks-progress";
private Path progressFileDir;
private Path progressFile;
private volatile int progress;
private WatchService watchService;
private Thread watchServiceThread;
private Set<Listener> observers;
public ProgressFileReader() {
// mainly for test purposes
observers = new HashSet<>(0);
}
public boolean start(File workingDir, TaskId taskId) {
String progressFileName = "job-" + taskId.getJobId().value() + "-task-" + taskId.value() + "-" +
UUID.randomUUID() + ".progress";
return start(workingDir, progressFileName);
}
boolean start(File workingDir, String filename) {
try {
createProgressFile(workingDir, filename);
watchService = FileSystems.getDefault().newWatchService();
watchServiceThread = new Thread(new ProgressFileReaderThread(filename));
watchServiceThread.setName(ProgressFileReaderThread.class.getName());
watchServiceThread.start();
progress = 0;
progressFileDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
return true;
} catch (IOException e) {
logger.warn("Error while creating progress file. Progress will not be reported.", e);
return false;
}
}
private void createProgressFile(File workingDir, String progressFileName) throws IOException {
progressFileDir = workingDir.toPath().resolve(PROGRESS_FILE_DIR);
try {
Files.createDirectories(progressFileDir);
ForkerUtils.setSharedExecutablePermissions(progressFileDir.toFile());
progressFile = progressFileDir.resolve(progressFileName);
Files.createFile(progressFile);
ForkerUtils.setSharedPermissions(progressFile.toFile());
} catch (FileAlreadyExistsException e) {
// ignore file already exists exception
}
logger.debug("Progress file '" + progressFile + "' created");
}
public int getProgress() {
return progress;
}
public Path getProgressFile() {
return progressFile;
}
public void register(Listener listener) {
this.observers.add(listener);
}
public void unregister(Listener listener) {
this.observers.remove(listener);
}
public void stop() {
if (watchService != null) {
try {
watchService.close();
watchServiceThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.info(e);
} catch (IOException e) {
logger.error(e);
} finally {
removeProgressFileDir();
}
}
}
private void removeProgressFileDir() {
if (progressFileDir == null || !Files.exists(progressFileDir)) {
return;
}
org.apache.commons.io.FileUtils.deleteQuietly(progressFileDir.toFile());
}
public interface Listener {
void onProgressUpdate(int newValue);
}
public final class ProgressFileReaderThread implements Runnable {
private final String progressFileName;
public ProgressFileReaderThread(String progressFileName) {
this.progressFileName = progressFileName;
}
@Override
public void run() {
FileTime lastModificationTime = FileTime.fromMillis(0);
try {
WatchKey watchKey;
while ((watchKey = watchService.take()) != null) {
// we have a polled event, now we traverse it and
// receive all the states from it
for (WatchEvent<?> event : watchKey.pollEvents()) {
WatchEvent<Path> ev = cast(event);
WatchEvent.Kind kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
Path path = ev.context();
// compare file modification time to prevent duplicate events since
// updating content and metadata may be detected as two independent update operations
FileTime newLastModificationTime = Files.getLastModifiedTime(progressFile);
if (newLastModificationTime.compareTo(lastModificationTime) > 0 &&
path.toString().equals(progressFileName)) {
readNewValue();
lastModificationTime = newLastModificationTime;
}
}
watchKey.reset();
}
} catch (InterruptedException e) {
logger.warn(e);
Thread.currentThread().interrupt();
} catch (IOException e) {
logger.warn(e);
} catch (ClosedWatchServiceException e) {
logger.debug("Watch service closed");
}
}
@SuppressWarnings("unchecked")
private WatchEvent<Path> cast(WatchEvent<?> event) {
return (WatchEvent<Path>) event;
}
private void readNewValue() {
try {
String line = com.google.common.io.Files.readFirstLine(progressFile.toFile(), Charset.defaultCharset());
if (line != null) {
try {
// try to parse double to allow int + double
int value = (int) Double.parseDouble(line);
if (value >= 0 && value <= 100) {
progress = value;
for (Listener observer : observers) {
observer.onProgressUpdate(progress);
}
if (logger.isDebugEnabled()) {
logger.debug("New progress value read: " + value);
}
} else {
logger.warn("Invalid progress value: " + value);
}
} catch (NumberFormatException e) {
logger.warn("Progress value is a not a numeric value: " + line);
}
}
} catch (IOException e) {
logger.warn("Error while reading the first line of " + progressFile);
}
}
}
}