mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Redesign policy running (#6609)
# Description of Changes Redesign policy running so the server is in charge of policy IDs and running, to make it impossible to have the frontend miss the results. This solves a minor bug that we currently have in policies, where if you load a file and then refresh while the policy is running, you'll never receive the outputted file.
This commit is contained in:
+31
@@ -36,6 +36,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.job.JobResponse;
|
||||
import stirling.software.common.service.JobOwnershipService;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.proprietary.policy.config.PolicyAccessGuard;
|
||||
@@ -73,6 +74,7 @@ public class PolicyController {
|
||||
private final PolicyManagementAuthority policyManagementAuthority;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final JobOwnershipService jobOwnershipService;
|
||||
|
||||
@PostMapping(value = "/run", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
@@ -143,6 +145,35 @@ public class PolicyController {
|
||||
return ResponseEntity.ok(PolicyRunView.of(run));
|
||||
}
|
||||
|
||||
@GetMapping("/runs")
|
||||
@Operation(
|
||||
summary = "List the caller's stored-policy runs",
|
||||
description =
|
||||
"Returns the caller's in-flight and recently-finished stored-policy runs (within"
|
||||
+ " the run-retention window). The frontend reconciles these on load so a"
|
||||
+ " run started before a refresh/crash is rediscovered and its outputs"
|
||||
+ " collected, rather than orphaned on the backend. Ad-hoc runs (no"
|
||||
+ " policy id) are excluded.")
|
||||
public List<PolicyRunView> listRuns() {
|
||||
return runRegistry.all().stream()
|
||||
.filter(run -> run.getPolicyId() != null)
|
||||
.filter(run -> ownedByCurrentUser(run.getRunId()))
|
||||
.map(PolicyRunView::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the run is owned by the current user, derived purely from the existing scoping
|
||||
* methods: stripping then re-applying the scope reproduces the run's key only when its owner
|
||||
* prefix matches the caller's. No auth (single-user) owns everything. Avoids duplicating the
|
||||
* scoped-key format here.
|
||||
*/
|
||||
private boolean ownedByCurrentUser(String runId) {
|
||||
return jobOwnershipService
|
||||
.createScopedJobKey(jobOwnershipService.extractJobId(runId))
|
||||
.equals(runId);
|
||||
}
|
||||
|
||||
// --- Policy management ---
|
||||
|
||||
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
|
||||
+18
-3
@@ -81,13 +81,27 @@ public class PolicyEngine {
|
||||
*/
|
||||
public PolicyRunHandle submit(
|
||||
PipelineDefinition definition, PolicyInputs inputs, PolicyProgressListener listener) {
|
||||
return submit(definition, inputs, listener, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #submit(PipelineDefinition, PolicyInputs, PolicyProgressListener)}, recording the
|
||||
* originating stored policy's id on the run ({@code null} for ad-hoc pipelines). The id lets a
|
||||
* client attribute a run it rediscovers via {@code GET /policies/runs} after losing local state
|
||||
* (e.g. a refresh before it recorded the run), so a finished run is never orphaned server-side.
|
||||
*/
|
||||
public PolicyRunHandle submit(
|
||||
PipelineDefinition definition,
|
||||
PolicyInputs inputs,
|
||||
PolicyProgressListener listener,
|
||||
String policyId) {
|
||||
// Ad-hoc run (no stored policy): bill whoever kicked it off and own the outputs as them
|
||||
// too.
|
||||
// Capture the principal on this (request) thread — it does not survive the hop onto the
|
||||
// async
|
||||
// worker.
|
||||
String principal = currentActingPrincipal();
|
||||
return submitForPrincipal(principal, principal, definition, inputs, listener);
|
||||
return submitForPrincipal(principal, principal, policyId, definition, inputs, listener);
|
||||
}
|
||||
|
||||
/** Run a stored policy on demand. {@code enabled} gates triggers, not explicit runs. */
|
||||
@@ -103,12 +117,13 @@ public class PolicyEngine {
|
||||
String triggeringUser = currentActingPrincipal();
|
||||
String fileOwner = triggeringUser != null ? triggeringUser : policy.owner();
|
||||
return submitForPrincipal(
|
||||
policy.owner(), fileOwner, policy.toDefinition(), inputs, listener);
|
||||
policy.owner(), fileOwner, policy.id(), policy.toDefinition(), inputs, listener);
|
||||
}
|
||||
|
||||
private PolicyRunHandle submitForPrincipal(
|
||||
String billingPrincipal,
|
||||
String fileOwner,
|
||||
String policyId,
|
||||
PipelineDefinition definition,
|
||||
PolicyInputs inputs,
|
||||
PolicyProgressListener listener) {
|
||||
@@ -116,7 +131,7 @@ public class PolicyEngine {
|
||||
// ownership check passes. No-op when security is off.
|
||||
String runId = jobOwnershipService.createScopedJobKey(UUID.randomUUID().toString());
|
||||
taskManager.createTask(runId);
|
||||
PolicyRun run = new PolicyRun(runId, definition);
|
||||
PolicyRun run = new PolicyRun(runId, policyId, definition);
|
||||
registry.register(run);
|
||||
CompletableFuture<PolicyRun> completion = new CompletableFuture<>();
|
||||
PolicyProgressListener tracking = trackingListener(runId, run, listener);
|
||||
|
||||
+6
-1
@@ -17,6 +17,10 @@ import stirling.software.common.model.job.ResultFile;
|
||||
public class PolicyRun {
|
||||
|
||||
private final String runId;
|
||||
|
||||
/** ID of the stored policy that produced this run; null for ad-hoc pipelines. */
|
||||
private final String policyId;
|
||||
|
||||
private final PipelineDefinition definition;
|
||||
private final Instant createdAt = Instant.now();
|
||||
|
||||
@@ -45,8 +49,9 @@ public class PolicyRun {
|
||||
private volatile List<ResultFile> outputs = List.of();
|
||||
private volatile Instant updatedAt = Instant.now();
|
||||
|
||||
public PolicyRun(String runId, PipelineDefinition definition) {
|
||||
public PolicyRun(String runId, String policyId, PipelineDefinition definition) {
|
||||
this.runId = runId;
|
||||
this.policyId = policyId;
|
||||
this.definition = definition;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -10,23 +10,28 @@ import stirling.software.common.model.job.ResultFile;
|
||||
*/
|
||||
public record PolicyRunView(
|
||||
String runId,
|
||||
String policyId,
|
||||
PolicyRunStatus status,
|
||||
int currentStep,
|
||||
int stepCount,
|
||||
String error,
|
||||
String errorCode,
|
||||
Boolean errorSubscribed,
|
||||
List<ResultFile> outputs) {
|
||||
List<ResultFile> outputs,
|
||||
/** When the run was created, epoch millis, so a rediscovered run shows its real age. */
|
||||
long createdAt) {
|
||||
|
||||
public static PolicyRunView of(PolicyRun run) {
|
||||
return new PolicyRunView(
|
||||
run.getRunId(),
|
||||
run.getPolicyId(),
|
||||
run.getStatus(),
|
||||
run.getCurrentStep(),
|
||||
run.stepCount(),
|
||||
run.getError(),
|
||||
run.getErrorCode(),
|
||||
run.getErrorSubscribed(),
|
||||
run.getOutputs());
|
||||
run.getOutputs(),
|
||||
run.getCreatedAt().toEpochMilli());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ class PolicyRunRegistryTest {
|
||||
}
|
||||
|
||||
private PolicyRun register(String runId) {
|
||||
PolicyRun run = new PolicyRun(runId, new PipelineDefinition(runId, List.of(), null));
|
||||
PolicyRun run = new PolicyRun(runId, null, new PipelineDefinition(runId, List.of(), null));
|
||||
registry.register(run);
|
||||
return run;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user