diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index dd6a3be5d..dba59cf15 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -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; @@ -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 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 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 { @@ -769,4 +841,10 @@ public RecordingNotFoundException(Pair key) { this(key.getLeft(), key.getRight()); } } + + static class SnapshotCreationException extends Exception { + public SnapshotCreationException(String message) { + super(message); + } + } } diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 843f4bf39..a9efb8488 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -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 @@ -576,7 +626,7 @@ public Response createRecording( } return Response.status(Response.Status.CREATED) - .entity(recordingHelper.toExternalForm(recording).toString()) + .entity(recordingHelper.toExternalForm(recording)) .build(); }