/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* 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.intellij.execution;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.newvfs.persistent.FlushingDaemon;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.IOUtil;
import com.intellij.util.io.PersistentHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
/**
* @author Dmitry Avdeev
*/
public class TestStateStorage implements Disposable {
private static final File TEST_HISTORY_PATH = new File(PathManager.getSystemPath(), "testHistory");
private static final int CURRENT_VERSION = 1;
private final File myFile;
public static File getTestHistoryRoot(Project project) {
return new File(TEST_HISTORY_PATH, project.getLocationHash());
}
public static class Record {
public final int magnitude;
public final long configurationHash;
public final Date date;
public Record(int magnitude, Date date, long configurationHash) {
this.magnitude = magnitude;
this.date = date;
this.configurationHash = configurationHash;
}
}
private static final Logger LOG = Logger.getInstance(TestStateStorage.class);
@Nullable
private PersistentHashMap<String, Record> myMap;
private volatile ScheduledFuture<?> myMapFlusher;
public static TestStateStorage getInstance(@NotNull Project project) {
return ServiceManager.getService(project, TestStateStorage.class);
}
public TestStateStorage(Project project) {
String directoryPath = getTestHistoryRoot(project).getPath();
myFile = new File(directoryPath + "/testStateMap");
FileUtilRt.createParentDirs(myFile);
try {
myMap = initializeMap();
} catch (IOException e) {
LOG.error(e);
}
myMapFlusher = FlushingDaemon.everyFiveSeconds(this::flushMap);
}
private PersistentHashMap<String, Record> initializeMap() throws IOException {
return IOUtil.openCleanOrResetBroken(getComputable(myFile), myFile);
}
private synchronized void flushMap() {
if (myMapFlusher == null) return; // disposed
if (myMap != null && myMap.isDirty()) myMap.force();
}
@NotNull
private static ThrowableComputable<PersistentHashMap<String, Record>, IOException> getComputable(final File file) {
return () -> new PersistentHashMap<>(file, EnumeratorStringDescriptor.INSTANCE, new DataExternalizer<Record>() {
@Override
public void save(@NotNull DataOutput out, Record value) throws IOException {
out.writeInt(value.magnitude);
out.writeLong(value.date.getTime());
out.writeLong(value.configurationHash);
}
@Override
public Record read(@NotNull DataInput in) throws IOException {
return new Record(in.readInt(), new Date(in.readLong()), in.readLong());
}
}, 4096, CURRENT_VERSION);
}
@Nullable
public synchronized Record getState(String testUrl) {
try {
return myMap == null ? null : myMap.get(testUrl);
}
catch (IOException e) {
thingsWentWrongLetsReinitialize(e, "Can't get state for " + testUrl);
return null;
}
}
public synchronized void removeState(String url) {
if (myMap != null) {
try {
myMap.remove(url);
}
catch (IOException e) {
thingsWentWrongLetsReinitialize(e, "Can't remove state for " + url);
}
}
}
@Nullable
public synchronized Map<String, Record> getRecentTests(int limit, Date since) {
if (myMap == null) return null;
Map<String, Record> result = ContainerUtil.newHashMap();
try {
for (String key : myMap.getAllKeysWithExistingMapping()) {
Record record = myMap.get(key);
if (record != null && record.date.compareTo(since) > 0) {
result.put(key, record);
if (result.size() >= limit) {
break;
}
}
}
}
catch (IOException e) {
thingsWentWrongLetsReinitialize(e, "Can't get recent tests");
}
return result;
}
public synchronized void writeState(@NotNull String testUrl, Record record) {
if (myMap == null) return;
try {
myMap.put(testUrl, record);
}
catch (IOException e) {
thingsWentWrongLetsReinitialize(e, "Can't write state for " + testUrl);
}
}
@Override
public synchronized void dispose() {
myMapFlusher.cancel(false);
myMapFlusher = null;
if (myMap == null) return;
try {
myMap.close();
}
catch (IOException e) {
LOG.error(e);
}
finally {
myMap = null;
}
}
private void thingsWentWrongLetsReinitialize(IOException e, String message) {
try {
if (myMap != null) {
try {
myMap.close();
}
catch (IOException ignore) {
}
IOUtil.deleteAllFilesStartingWith(myFile);
}
myMap = initializeMap();
LOG.error(message, e);
}
catch (IOException e1) {
LOG.error("Cannot repair", e1);
myMap = null;
}
}
}