Skip to content

Commit

Permalink
feat(recordings): implement JFR snapshot
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewazores committed Nov 21, 2023
1 parent de4db51 commit 012696a
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 1 deletion.
78 changes: 78 additions & 0 deletions src/main/java/io/cryostat/recordings/RecordingHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -195,6 +196,77 @@ public ActiveRecording startRecording(
return recording;
}

public ActiveRecording createSnapshot(Target target, JFRConnection connection)
throws Exception {
IRecordingDescriptor desc = connection.getService().getSnapshotRecording();

String rename = String.format("%s-%d", desc.getName().toLowerCase(), desc.getId());

RecordingOptionsBuilder recordingOptionsBuilder =
recordingOptionsBuilderFactory.create(connection.getService());
recordingOptionsBuilder.name(rename);

connection.getService().updateRecordingOptions(desc, recordingOptionsBuilder.build());

Optional<IRecordingDescriptor> updatedDescriptor = getDescriptorByName(connection, rename);

if (updatedDescriptor.isEmpty()) {
throw new IllegalStateException(
"The most recent snapshot of the recording cannot be"
+ " found after renaming.");
}

desc = updatedDescriptor.get();

ActiveRecording recording =
ActiveRecording.from(
target,
desc,
new Metadata(
Map.of(
"jvmId",
target.jvmId,
"connectUrl",
target.connectUrl.toString())));
recording.persist();

target.activeRecordings.add(recording);
target.persist();

try (InputStream snapshot = getActiveInputStream(target.id, desc.getId())) {
if (!snapshotIsReadable(target, snapshot)) {
String snapshotName = desc.getName();
this.deleteRecording(target, r -> Objects.equals(r.name, snapshotName));
throw new SnapshotCreationException(
"Snapshot was not readable - are there any source recordings?");
}
}

bus.publish(
MessagingServer.class.getName(),
new Notification(
"SnapshotCreated", new RecordingEvent(target.connectUrl, recording)));

return recording;
}

public void deleteRecording(Target target, Predicate<ActiveRecording> predicate) {
target.activeRecordings.stream().filter(predicate).forEach(ActiveRecording::delete);
}

private boolean snapshotIsReadable(Target target, InputStream snapshot) throws IOException {
if (!connectionManager.markConnectionInUse(target)) {
throw new IOException(
"Target connection unexpectedly closed while streaming recording");
}

try {
return snapshot.read() != -1;
} catch (IOException e) {
return false;
}
}

private boolean shouldRestartRecording(
RecordingReplace replace, RecordingState state, String recordingName)
throws BadRequestException {
Expand Down Expand Up @@ -769,4 +841,10 @@ public RecordingNotFoundException(Pair<String, String> key) {
this(key.getLeft(), key.getRight());
}
}

static class SnapshotCreationException extends Exception {
public SnapshotCreationException(String message) {
super(message);
}
}
}
52 changes: 51 additions & 1 deletion src/main/java/io/cryostat/recordings/Recordings.java
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,56 @@ public Response patchV1(@RestPath URI connectUrl, @RestPath String recordingName
.build();
}

@POST
@Transactional
@Blocking
@Path("/api/v1/targets/{connectUrl}/snapshot")
@RolesAllowed("write")
public Response createSnapshotV1(@RestPath URI connectUrl) {
return Response.status(RestResponse.Status.PERMANENT_REDIRECT)
.location(
URI.create(
String.format(
"/api/v3/targets/%d/snapshot",
Target.getTargetByConnectUrl(connectUrl).id)))
.build();
}

@POST
@Transactional
@Blocking
@Path("/api/v2/targets/{connectUrl}/snapshot")
@RolesAllowed("write")
public Response createSnapshotV2(@RestPath URI connectUrl) {
return Response.status(RestResponse.Status.PERMANENT_REDIRECT)
.location(
URI.create(
String.format(
"/api/v3/targets/%d/snapshot",
Target.getTargetByConnectUrl(connectUrl).id)))
.build();
}

@POST
@Transactional
@Blocking
@Path("/api/v3/targets/{id}/snapshot")
@RolesAllowed("write")
public Response createSnapshot(@RestPath long id) throws Exception {
Target target = Target.find("id", id).singleResult();
try {
ActiveRecording recording =
connectionManager.executeConnectedTask(
target,
connection -> recordingHelper.createSnapshot(target, connection));
return Response.status(Response.Status.OK)
.entity(recordingHelper.toExternalForm(recording))
.build();
} catch (RecordingHelper.SnapshotCreationException sce) {
return Response.status(Response.Status.ACCEPTED).build();
}
}

@POST
@Transactional
@Blocking
Expand Down Expand Up @@ -576,7 +626,7 @@ public Response createRecording(
}

return Response.status(Response.Status.CREATED)
.entity(recordingHelper.toExternalForm(recording).toString())
.entity(recordingHelper.toExternalForm(recording))
.build();
}

Expand Down

0 comments on commit 012696a

Please sign in to comment.