Skip to content

Commit

Permalink
fix(auto-rules): PeriodicArchiver scans archives on startup (#551)
Browse files Browse the repository at this point in the history
* Retrieve list of recordings saved in archive

* Start parsing archived recordings JSON

* Refactor previous recordings extraction to be outside of the constructor

* Iterate through archive to get previous recordings for the current rule

* Extract archived recordings listing into refactored chunk

* Clean up unneeded imports and class variables

* Start iteration of archived recordings

* Continue iteration of archived recordings

* Begin archive directory restructuring

* Implement get archive listing using Pattern matching

* Begin refactoring related files to accommodate archive directory change

* Implement nested directory structure using Path instead of String

* Update archived recording(s) GET handlers

* Create new class to represent archived recording information

* Update recording archival and GET handlers to work with new class

* Fix compilation errors related to exception handling and class instantiation

* Remove unnecessary Future creation/handling in PeriodicArchiver

* fixup! Remove unnecessary Future creation/handling in PeriodicArchiver

* Update archived recordings GET and DELETE handlers to work with directory restructuring

* Update archived recordings POST handlers to work with directory restructuring

* Begin updating unit tests

* Fix test fields and comment out certain tests to allow debugging

* Update archived recordings GET handler test

* Extract ArchivedRecordingInfo custom JSON serializer into its own class; run mvn spotless:apply

* Update old PeriodicArchiver unit tests to better reflect its class structure

* fixup! Extract ArchivedRecordingInfo custom JSON serializer into its own class; run mvn spotless:apply

* Change the RecordingArchiveHelper implementation to be Future-based and update any corresponding tests

* Update RecordingArchiveHelperTest

* Update RecordingsPostHandlerTest

* Fix broken unit tests

* Update PeriodicArchiverTest to include archive scanning test

* Test JSON serialization of archived recordings

* Decouple target recording deletion from the RecordingArchiveHelper

* Extract the correct archived recording deletion code from the RecordingDeleteHandler

* Run mvn spotless:apply

* fixup! Run mvn spotless:apply

* Fix IOException due to not creating the encoded service URI directory

* Fix unit test broken by addition of encoded service URI directory

* Update archived recordings GET implementation and testing to reflect proper directory hierarchy

* Update archived recordings DELETE implementation to reflect proper directory hierarchy

* Update archived recordings DELETE testing to reflect proper directory hierarchy

* Extract single recording GET into the RecordingArchiveHelper and update to reflect proper directory hierarchy

* Update RecordingWorkflowIT to verify target and saved recordings deletion

* Run mvn spotless:apply

* Remove reliance on default encoding

* Remove unnecessary saved recordings path from archived recording GET handler

* Update archived recordings POST handler to reflect proper directory hierarchy

* Update archived recordings POST handler testing to reflect proper directory hierarchy

* Update archived recordings UPLOAD POST handler implementation and testing to reflect proper directory hierarchy

* fixup! Update archived recordings UPLOAD POST() handler implementation and testing to reflect proper directory hierarchy

* Run mvn spotless:apply

* Remove unnecessary IOException handling

* Update archived recording GET testing

* Add missing status code field to archived recordings GET handler

* Continue removing unnecessary IOException handling

* Remove deprecated import

* Fix unit test errors due to addition of default HTTP content type

* Fix incorrect CompletableFuture.get() error handling

* Run mvn spotless:apply

* fixup! Fix incorrect CompletableFuture.get() error handling

* Remove unnecessary status code setting and extract PATCH/DELETE notifications into the RecordingArchiveHelper

* Change Base32 instances from local to class level

* Normalize and convert to absolute path beforehand; refactor recording path getter function

* fixup! Normalize and convert to absolute path beforehand; refactor recording path getter function

* Delete deprecated saved recording descriptor class

* Revert changes made to RecordingWorkflowIT that cause intermittent failure

* Replace iterator with for-each loop

* Remove unnecessary custom JSON serialization

* Ensure handlers throw HttpStatusExceptions containing the most information possible

* Encapsulate specific Exceptions inside Future(s) in the RecordingArchiveHelper API

* Clean-up exception handling

* Include deleted recording's path in returned Future

* Start fixing incorrect directory search during archived recording report caching

* Extract archived recording report deletion into the RecordingArchiveHelper; replace old, static RecordingNotFoundException class with new, separate class

* fixup! Clean-up exception handling

* Fix incorrect subdirectory creation in handler without access to targetID

* Clean-up exception handling

* Remove unecessary HTTP response end()

* Run mvn spotless:apply

* fixup! Remove unecessary HTTP response end()

* Fix incorrect AuthManager mocking in unit tests

* Run mvn spotless:apply
  • Loading branch information
Hareet Dhillon committed Aug 11, 2021
1 parent a703e48 commit 2a26702
Show file tree
Hide file tree
Showing 39 changed files with 1,288 additions and 885 deletions.
Expand Up @@ -47,105 +47,74 @@
import javax.inject.Named;
import javax.inject.Provider;

import io.cryostat.MainModule;
import io.cryostat.core.log.Logger;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.net.reports.ReportService.RecordingNotFoundException;
import io.cryostat.net.web.WebModule;
import io.cryostat.net.web.http.generic.TimeoutHandler;
import io.cryostat.recordings.RecordingArchiveHelper;

class ArchivedRecordingReportCache {

protected final Path savedRecordingsPath;
protected final Path archivedRecordingsReportPath;
protected final FileSystem fs;
protected final Provider<SubprocessReportGenerator> subprocessReportGeneratorProvider;
protected final ReentrantLock generationLock;
protected final Logger logger;
protected final RecordingArchiveHelper recordingArchiveHelper;

ArchivedRecordingReportCache(
@Named(MainModule.RECORDINGS_PATH) Path savedRecordingsPath,
@Named(WebModule.WEBSERVER_TEMP_DIR_PATH) Path webServerTempPath,
FileSystem fs,
Provider<SubprocessReportGenerator> subprocessReportGeneratorProvider,
@Named(ReportsModule.REPORT_GENERATION_LOCK) ReentrantLock generationLock,
Logger logger) {
this.savedRecordingsPath = savedRecordingsPath;
this.archivedRecordingsReportPath = webServerTempPath;
Logger logger,
RecordingArchiveHelper recordingArchiveHelper) {
this.fs = fs;
this.subprocessReportGeneratorProvider = subprocessReportGeneratorProvider;
this.generationLock = generationLock;
this.logger = logger;
this.recordingArchiveHelper = recordingArchiveHelper;
}

Future<Path> get(String recordingName) {
CompletableFuture<Path> f = new CompletableFuture<>();
Path dest = getCachedReportPath(recordingName);
Path dest = recordingArchiveHelper.getCachedReportPath(recordingName);
if (fs.isReadable(dest) && fs.isRegularFile(dest)) {
f.complete(dest);
return f;
}

try {
generationLock.lock();
// check again in case the previous lock holder already created the cached file
if (fs.isReadable(dest) && fs.isRegularFile(dest)) {
f.complete(dest);
return f;
}
logger.trace("Archived report cache miss for {}", recordingName);

fs.listDirectoryChildren(savedRecordingsPath).stream()
.filter(name -> name.equals(recordingName))
.map(savedRecordingsPath::resolve)
.findFirst()
.ifPresentOrElse(
recording -> {
logger.trace("Archived report cache miss for {}", recordingName);
try {
Path saveFile =
subprocessReportGeneratorProvider
.get()
.exec(
recording,
dest,
Duration.ofMillis(
TimeoutHandler.TIMEOUT_MS))
.get();
f.complete(saveFile);
} catch (Exception e) {
logger.error(e);
f.completeExceptionally(e);
try {
fs.deleteIfExists(dest);
} catch (IOException ioe) {
logger.warn(ioe);
}
}
},
() ->
f.completeExceptionally(
new RecordingNotFoundException(
"archives", recordingName)));
} catch (IOException ioe) {
logger.warn(ioe);
f.completeExceptionally(ioe);
Path archivedRecording = recordingArchiveHelper.getRecordingPath(recordingName).get();
Path saveFile =
subprocessReportGeneratorProvider
.get()
.exec(
archivedRecording,
dest,
Duration.ofMillis(TimeoutHandler.TIMEOUT_MS))
.get();
f.complete(saveFile);
} catch (Exception e) {
logger.error(e);
f.completeExceptionally(e);
try {
fs.deleteIfExists(dest);
} catch (IOException ioe) {
logger.warn(ioe);
}
} finally {
generationLock.unlock();
}
return f;
}

boolean delete(String recordingName) {
try {
logger.trace("Invalidating archived report cache for {}", recordingName);
return fs.deleteIfExists(getCachedReportPath(recordingName));
} catch (IOException ioe) {
logger.warn(ioe);
return false;
}
}

protected Path getCachedReportPath(String recordingName) {
String fileName = recordingName + ".report.html";
return archivedRecordingsReportPath.resolve(fileName).toAbsolutePath();
return recordingArchiveHelper.deleteReport(recordingName);
}
}
13 changes: 0 additions & 13 deletions src/main/java/io/cryostat/net/reports/ReportService.java
Expand Up @@ -42,8 +42,6 @@

import io.cryostat.net.ConnectionDescriptor;

import org.apache.commons.lang3.tuple.Pair;

public class ReportService {

private final ActiveRecordingReportCache activeCache;
Expand All @@ -70,15 +68,4 @@ public Future<String> get(ConnectionDescriptor connectionDescriptor, String reco
public boolean delete(ConnectionDescriptor connectionDescriptor, String recordingName) {
return activeCache.delete(connectionDescriptor, recordingName);
}

// FIXME This is basically duplicated from UploadRecordingCommand
public static class RecordingNotFoundException extends RuntimeException {
public RecordingNotFoundException(String targetId, String recordingName) {
super(String.format("Recording %s not found in target %s", recordingName, targetId));
}

public RecordingNotFoundException(Pair<String, String> key) {
this(key.getLeft(), key.getRight());
}
}
}
13 changes: 5 additions & 8 deletions src/main/java/io/cryostat/net/reports/ReportsModule.java
Expand Up @@ -47,13 +47,12 @@
import javax.inject.Provider;
import javax.inject.Singleton;

import io.cryostat.MainModule;
import io.cryostat.core.log.Logger;
import io.cryostat.core.reports.ReportTransformer;
import io.cryostat.core.sys.Environment;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.net.TargetConnectionManager;
import io.cryostat.net.web.WebModule;
import io.cryostat.recordings.RecordingArchiveHelper;
import io.cryostat.util.JavaProcess;

import dagger.Module;
Expand Down Expand Up @@ -126,19 +125,17 @@ static SubprocessReportGenerator provideSubprocessReportGenerator(
@Provides
@Singleton
static ArchivedRecordingReportCache provideArchivedRecordingReportCache(
@Named(MainModule.RECORDINGS_PATH) Path savedRecordingsPath,
@Named(WebModule.WEBSERVER_TEMP_DIR_PATH) Path webServerTempDir,
FileSystem fs,
Provider<SubprocessReportGenerator> subprocessReportGeneratorProvider,
@Named(REPORT_GENERATION_LOCK) ReentrantLock generationLock,
Logger logger) {
Logger logger,
RecordingArchiveHelper recordingArchiveHelper) {
return new ArchivedRecordingReportCache(
savedRecordingsPath,
webServerTempDir,
fs,
subprocessReportGeneratorProvider,
generationLock,
logger);
logger,
recordingArchiveHelper);
}

@Provides
Expand Down
Expand Up @@ -71,7 +71,7 @@
import io.cryostat.core.sys.FileSystem;
import io.cryostat.net.ConnectionDescriptor;
import io.cryostat.net.TargetConnectionManager;
import io.cryostat.net.reports.ReportService.RecordingNotFoundException;
import io.cryostat.recordings.RecordingNotFoundException;
import io.cryostat.util.JavaProcess;

import org.apache.commons.lang3.builder.EqualsBuilder;
Expand Down
Expand Up @@ -37,7 +37,6 @@
*/
package io.cryostat.net.web.http.api.v1;

import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.net.TargetConnectionManager;
import io.cryostat.net.web.http.RequestHandler;
import io.cryostat.recordings.RecordingArchiveHelper;
Expand All @@ -46,6 +45,7 @@
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import org.apache.commons.codec.binary.Base32;

@Module
public abstract class HttpApiV1Module {
Expand Down Expand Up @@ -92,9 +92,8 @@ abstract RequestHandler bindTargetRecordingPatchBodyHandler(

@Provides
static TargetRecordingPatchSave provideTargetRecordingPatchSave(
RecordingArchiveHelper recordingArchiveHelper,
NotificationFactory notificationFactory) {
return new TargetRecordingPatchSave(recordingArchiveHelper, notificationFactory);
RecordingArchiveHelper recordingArchiveHelper) {
return new TargetRecordingPatchSave(recordingArchiveHelper);
}

@Provides
Expand Down Expand Up @@ -135,6 +134,11 @@ static TargetRecordingPatchStop provideTargetRecordingPatchStop(
@IntoSet
abstract RequestHandler bindRecordingsPostHandler(RecordingsPostHandler handler);

@Provides
static Base32 provideBase32() {
return new Base32();
}

@Binds
@IntoSet
abstract RequestHandler bindTargetsGetHandler(TargetsGetHandler handler);
Expand Down
Expand Up @@ -37,49 +37,32 @@
*/
package io.cryostat.net.web.http.api.v1;

import java.io.IOException;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import javax.inject.Inject;
import javax.inject.Named;

import io.cryostat.MainModule;
import io.cryostat.core.sys.FileSystem;
import io.cryostat.messaging.notifications.NotificationFactory;
import io.cryostat.net.AuthManager;
import io.cryostat.net.reports.ReportService;
import io.cryostat.net.security.ResourceAction;
import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
import io.cryostat.net.web.http.HttpMimeType;
import io.cryostat.net.web.http.api.ApiVersion;
import io.cryostat.recordings.RecordingArchiveHelper;
import io.cryostat.recordings.RecordingNotFoundException;

import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.impl.HttpStatusException;
import org.apache.commons.lang3.exception.ExceptionUtils;

public class RecordingDeleteHandler extends AbstractAuthenticatedRequestHandler {

private final ReportService reportService;
private final FileSystem fs;
private final Path savedRecordingsPath;
private final NotificationFactory notificationFactory;
private static final String NOTIFICATION_CATEGORY = "RecordingDeleted";
private final RecordingArchiveHelper recordingArchiveHelper;

@Inject
RecordingDeleteHandler(
AuthManager auth,
ReportService reportService,
FileSystem fs,
NotificationFactory notificationFactory,
@Named(MainModule.RECORDINGS_PATH) Path savedRecordingsPath) {
RecordingDeleteHandler(AuthManager auth, RecordingArchiveHelper recordingArchiveHelper) {
super(auth);
this.reportService = reportService;
this.fs = fs;
this.savedRecordingsPath = savedRecordingsPath;
this.notificationFactory = notificationFactory;
this.recordingArchiveHelper = recordingArchiveHelper;
}

@Override
Expand Down Expand Up @@ -110,34 +93,14 @@ public boolean isAsync() {
@Override
public void handleAuthenticated(RoutingContext ctx) throws Exception {
String recordingName = ctx.pathParam("recordingName");
fs.listDirectoryChildren(savedRecordingsPath).stream()
.filter(saved -> saved.equals(recordingName))
.map(savedRecordingsPath::resolve)
.findFirst()
.ifPresentOrElse(
path -> {
try {
if (!fs.exists(path)) {
throw new HttpStatusException(404, recordingName);
}
fs.deleteIfExists(path);
notificationFactory
.createBuilder()
.metaCategory(NOTIFICATION_CATEGORY)
.metaType(HttpMimeType.JSON)
.message(Map.of("recording", recordingName))
.build()
.send();
} catch (IOException e) {
throw new HttpStatusException(500, e.getMessage(), e);
} finally {
reportService.delete(recordingName);
}
ctx.response().setStatusCode(200);
ctx.response().end();
},
() -> {
throw new HttpStatusException(404, recordingName);
});
try {
recordingArchiveHelper.deleteRecording(recordingName).get();
ctx.response().end();
} catch (ExecutionException e) {
if (ExceptionUtils.getRootCause(e) instanceof RecordingNotFoundException) {
throw new HttpStatusException(404, e.getMessage(), e);
}
throw e;
}
}
}

0 comments on commit 2a26702

Please sign in to comment.