forked from jsl/video_editing_poc
Add managed local CV worker startup
This commit is contained in:
parent
9dc720b0cc
commit
e2f49e2e5a
|
|
@ -4,16 +4,17 @@ This service can use a local computer-vision worker for highlight visual analysi
|
||||||
|
|
||||||
## Default Behavior
|
## Default Behavior
|
||||||
|
|
||||||
By default the service uses the built-in heuristic provider:
|
The packaged `application.yml` defaults to `local-cv` with heuristic fallback enabled:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
video-clipping:
|
video-clipping:
|
||||||
editing:
|
editing:
|
||||||
visual-analysis:
|
visual-analysis:
|
||||||
provider: heuristic
|
provider: local-cv
|
||||||
|
fallback-to-heuristic: true
|
||||||
```
|
```
|
||||||
|
|
||||||
This keeps the app runnable without Python, GPU drivers, model downloads, or a separate service.
|
This keeps the app runnable when the local worker is unavailable, but visual analysis is stronger when the worker is running.
|
||||||
|
|
||||||
## Enable A Local CV Worker
|
## Enable A Local CV Worker
|
||||||
|
|
||||||
|
|
@ -27,6 +28,10 @@ video-clipping:
|
||||||
endpoint: http://127.0.0.1:8091/v1/analyze-visuals
|
endpoint: http://127.0.0.1:8091/v1/analyze-visuals
|
||||||
timeout-ms: 30000
|
timeout-ms: 30000
|
||||||
fallback-to-heuristic: true
|
fallback-to-heuristic: true
|
||||||
|
local-cv-worker:
|
||||||
|
auto-start: false
|
||||||
|
script: ./tools/run_local_cv_worker.sh
|
||||||
|
startup-wait-ms: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
Equivalent environment variables:
|
Equivalent environment variables:
|
||||||
|
|
@ -36,6 +41,9 @@ VIDEO_EDITING_VISUAL_ANALYSIS_PROVIDER=local-cv
|
||||||
VIDEO_EDITING_VISUAL_ANALYSIS_ENDPOINT=http://127.0.0.1:8091/v1/analyze-visuals
|
VIDEO_EDITING_VISUAL_ANALYSIS_ENDPOINT=http://127.0.0.1:8091/v1/analyze-visuals
|
||||||
VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS=30000
|
VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS=30000
|
||||||
VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC=true
|
VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC=true
|
||||||
|
VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START=false
|
||||||
|
VIDEO_EDITING_LOCAL_CV_WORKER_SCRIPT=./tools/run_local_cv_worker.sh
|
||||||
|
VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS=0
|
||||||
```
|
```
|
||||||
|
|
||||||
This repository includes an optional starter worker:
|
This repository includes an optional starter worker:
|
||||||
|
|
@ -60,6 +68,20 @@ Health check:
|
||||||
curl http://127.0.0.1:8091/health
|
curl http://127.0.0.1:8091/health
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Spring-Managed Worker Startup
|
||||||
|
|
||||||
|
The Spring application can start the local CV worker for local runs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VIDEO_EDITING_VISUAL_ANALYSIS_PROVIDER=local-cv \
|
||||||
|
VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START=true \
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
When `auto-start` is enabled, Spring starts `tools/run_local_cv_worker.sh`, derives `LOCAL_CV_HOST` and `LOCAL_CV_PORT` from the configured visual-analysis endpoint, streams worker output into application logs, and stops the worker process during application shutdown.
|
||||||
|
|
||||||
|
Keep `auto-start` disabled in production unless the app host is intended to own the Python worker lifecycle. The first run can take time because the script creates `.venv-local-cv`, installs Python dependencies, and may download/cache the configured YOLO model.
|
||||||
|
|
||||||
## Worker Request Contract
|
## Worker Request Contract
|
||||||
|
|
||||||
The Spring service sends:
|
The Spring service sends:
|
||||||
|
|
|
||||||
|
|
@ -858,6 +858,8 @@ public class VideoClippingProperties {
|
||||||
|
|
||||||
private boolean fallbackToHeuristic = true;
|
private boolean fallbackToHeuristic = true;
|
||||||
|
|
||||||
|
private final LocalCvWorker localCvWorker = new LocalCvWorker();
|
||||||
|
|
||||||
public String getProvider() {
|
public String getProvider() {
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
@ -889,6 +891,43 @@ public class VideoClippingProperties {
|
||||||
public void setFallbackToHeuristic(boolean fallbackToHeuristic) {
|
public void setFallbackToHeuristic(boolean fallbackToHeuristic) {
|
||||||
this.fallbackToHeuristic = fallbackToHeuristic;
|
this.fallbackToHeuristic = fallbackToHeuristic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocalCvWorker getLocalCvWorker() {
|
||||||
|
return localCvWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalCvWorker {
|
||||||
|
private boolean autoStart = false;
|
||||||
|
|
||||||
|
private String script = "./tools/run_local_cv_worker.sh";
|
||||||
|
|
||||||
|
@Min(0)
|
||||||
|
private long startupWaitMs = 0;
|
||||||
|
|
||||||
|
public boolean isAutoStart() {
|
||||||
|
return autoStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutoStart(boolean autoStart) {
|
||||||
|
this.autoStart = autoStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScript() {
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScript(String script) {
|
||||||
|
this.script = script;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStartupWaitMs() {
|
||||||
|
return startupWaitMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartupWaitMs(long startupWaitMs) {
|
||||||
|
this.startupWaitMs = startupWaitMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class LocalDirector {
|
public static class LocalDirector {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
package org.example.videoclips.editing;
|
||||||
|
|
||||||
|
import org.example.videoclips.config.VideoClippingProperties;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.SmartLifecycle;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(name = "video-clipping.editing.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LocalCvWorkerProcessManager.class);
|
||||||
|
|
||||||
|
private final VideoClippingProperties.Editing.VisualAnalysis visualAnalysis;
|
||||||
|
private final ProcessLauncher processLauncher;
|
||||||
|
private Process process;
|
||||||
|
private Thread outputThread;
|
||||||
|
private boolean running;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public LocalCvWorkerProcessManager(VideoClippingProperties properties) {
|
||||||
|
this(properties, ProcessBuilder::start);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalCvWorkerProcessManager(
|
||||||
|
VideoClippingProperties properties,
|
||||||
|
ProcessLauncher processLauncher
|
||||||
|
) {
|
||||||
|
this.visualAnalysis = properties.getEditing().getVisualAnalysis();
|
||||||
|
this.processLauncher = processLauncher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void start() {
|
||||||
|
if (running || !shouldStart()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoClippingProperties.Editing.VisualAnalysis.LocalCvWorker worker = visualAnalysis.getLocalCvWorker();
|
||||||
|
Path script = Path.of(worker.getScript()).toAbsolutePath().normalize();
|
||||||
|
if (!Files.isRegularFile(script)) {
|
||||||
|
throw new IllegalStateException("Local CV worker script does not exist: " + script);
|
||||||
|
}
|
||||||
|
|
||||||
|
Endpoint endpoint = endpoint();
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(script.toString())
|
||||||
|
.redirectErrorStream(true);
|
||||||
|
Map<String, String> environment = processBuilder.environment();
|
||||||
|
environment.put("LOCAL_CV_HOST", endpoint.host());
|
||||||
|
environment.put("LOCAL_CV_PORT", Integer.toString(endpoint.port()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
process = processLauncher.start(processBuilder);
|
||||||
|
running = true;
|
||||||
|
outputThread = outputReader(process.getInputStream(), script);
|
||||||
|
outputThread.start();
|
||||||
|
log.info("event=local_cv_worker_started script={} pid={} host={} port={}",
|
||||||
|
script, process.pid(), endpoint.host(), endpoint.port());
|
||||||
|
waitForStartupWindow(worker.getStartupWaitMs());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
running = false;
|
||||||
|
throw new IllegalStateException("Unable to start local CV worker script: " + script, ex);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
cleanupFailedStart();
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void stop() {
|
||||||
|
if (process == null) {
|
||||||
|
running = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long pid = process.pid();
|
||||||
|
if (process.isAlive()) {
|
||||||
|
process.destroy();
|
||||||
|
try {
|
||||||
|
if (!process.waitFor(Duration.ofSeconds(5).toMillis(), TimeUnit.MILLISECONDS)) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
running = false;
|
||||||
|
log.info("event=local_cv_worker_stopped pid={}", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRunning() {
|
||||||
|
return running && process != null && process.isAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAutoStartup() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPhase() {
|
||||||
|
return Integer.MIN_VALUE + 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldStart() {
|
||||||
|
return "local-cv".equalsIgnoreCase(visualAnalysis.getProvider())
|
||||||
|
&& visualAnalysis.getLocalCvWorker().isAutoStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForStartupWindow(long startupWaitMs) {
|
||||||
|
if (startupWaitMs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(startupWaitMs);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("Interrupted while waiting for local CV worker startup", ex);
|
||||||
|
}
|
||||||
|
if (process != null && !process.isAlive()) {
|
||||||
|
throw new IllegalStateException("Local CV worker exited during startup window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupFailedStart() {
|
||||||
|
running = false;
|
||||||
|
if (process != null && process.isAlive()) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Endpoint endpoint() {
|
||||||
|
URI uri = URI.create(visualAnalysis.getEndpoint());
|
||||||
|
String host = uri.getHost() == null || uri.getHost().isBlank() ? "127.0.0.1" : uri.getHost();
|
||||||
|
int port = uri.getPort();
|
||||||
|
if (port < 0) {
|
||||||
|
port = "https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
||||||
|
}
|
||||||
|
return new Endpoint(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread outputReader(InputStream inputStream, Path script) {
|
||||||
|
Thread thread = new Thread(() -> {
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
log.info("event=local_cv_worker_output script={} message={}", script, line);
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.debug("event=local_cv_worker_output_closed script={} error_type={}",
|
||||||
|
script, ex.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}, "local-cv-worker-output");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
record Endpoint(String host, int port) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ProcessLauncher {
|
||||||
|
Process start(ProcessBuilder processBuilder) throws IOException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,10 @@ video-clipping:
|
||||||
endpoint: ${VIDEO_EDITING_VISUAL_ANALYSIS_ENDPOINT:http://127.0.0.1:8091/v1/analyze-visuals}
|
endpoint: ${VIDEO_EDITING_VISUAL_ANALYSIS_ENDPOINT:http://127.0.0.1:8091/v1/analyze-visuals}
|
||||||
timeout-ms: ${VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS:30000}
|
timeout-ms: ${VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS:30000}
|
||||||
fallback-to-heuristic: ${VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC:true}
|
fallback-to-heuristic: ${VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC:true}
|
||||||
|
local-cv-worker:
|
||||||
|
auto-start: ${VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START:false}
|
||||||
|
script: ${VIDEO_EDITING_LOCAL_CV_WORKER_SCRIPT:./tools/run_local_cv_worker.sh}
|
||||||
|
startup-wait-ms: ${VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS:0}
|
||||||
local-director:
|
local-director:
|
||||||
enabled: ${VIDEO_EDITING_LOCAL_DIRECTOR_ENABLED:true}
|
enabled: ${VIDEO_EDITING_LOCAL_DIRECTOR_ENABLED:true}
|
||||||
source-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_SOURCE_DIRECTORY:./input/editing/source}
|
source-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_SOURCE_DIRECTORY:./input/editing/source}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ class VideoClippingPropertiesTest {
|
||||||
.isEqualTo("http://127.0.0.1:8091/v1/analyze-visuals");
|
.isEqualTo("http://127.0.0.1:8091/v1/analyze-visuals");
|
||||||
assertThat(properties.getEditing().getVisualAnalysis().getTimeoutMs()).isEqualTo(30000);
|
assertThat(properties.getEditing().getVisualAnalysis().getTimeoutMs()).isEqualTo(30000);
|
||||||
assertThat(properties.getEditing().getVisualAnalysis().isFallbackToHeuristic()).isTrue();
|
assertThat(properties.getEditing().getVisualAnalysis().isFallbackToHeuristic()).isTrue();
|
||||||
|
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().isAutoStart()).isFalse();
|
||||||
|
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getScript())
|
||||||
|
.isEqualTo("./tools/run_local_cv_worker.sh");
|
||||||
|
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getStartupWaitMs()).isZero();
|
||||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isTrue();
|
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isTrue();
|
||||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory())
|
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory())
|
||||||
.isEqualTo("./input/editing/source");
|
.isEqualTo("./input/editing/source");
|
||||||
|
|
@ -101,6 +105,9 @@ class VideoClippingPropertiesTest {
|
||||||
"video-clipping.editing.visual-analysis.endpoint=http://localhost:9000/analyze",
|
"video-clipping.editing.visual-analysis.endpoint=http://localhost:9000/analyze",
|
||||||
"video-clipping.editing.visual-analysis.timeout-ms=12000",
|
"video-clipping.editing.visual-analysis.timeout-ms=12000",
|
||||||
"video-clipping.editing.visual-analysis.fallback-to-heuristic=false",
|
"video-clipping.editing.visual-analysis.fallback-to-heuristic=false",
|
||||||
|
"video-clipping.editing.visual-analysis.local-cv-worker.auto-start=true",
|
||||||
|
"video-clipping.editing.visual-analysis.local-cv-worker.script=/tmp/local-cv.sh",
|
||||||
|
"video-clipping.editing.visual-analysis.local-cv-worker.startup-wait-ms=250",
|
||||||
"video-clipping.editing.local-director.enabled=false",
|
"video-clipping.editing.local-director.enabled=false",
|
||||||
"video-clipping.editing.local-director.source-directory=/tmp/source",
|
"video-clipping.editing.local-director.source-directory=/tmp/source",
|
||||||
"video-clipping.editing.local-director.poll-interval-ms=9000",
|
"video-clipping.editing.local-director.poll-interval-ms=9000",
|
||||||
|
|
@ -140,6 +147,11 @@ class VideoClippingPropertiesTest {
|
||||||
.isEqualTo("http://localhost:9000/analyze");
|
.isEqualTo("http://localhost:9000/analyze");
|
||||||
assertThat(properties.getEditing().getVisualAnalysis().getTimeoutMs()).isEqualTo(12000);
|
assertThat(properties.getEditing().getVisualAnalysis().getTimeoutMs()).isEqualTo(12000);
|
||||||
assertThat(properties.getEditing().getVisualAnalysis().isFallbackToHeuristic()).isFalse();
|
assertThat(properties.getEditing().getVisualAnalysis().isFallbackToHeuristic()).isFalse();
|
||||||
|
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().isAutoStart()).isTrue();
|
||||||
|
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getScript())
|
||||||
|
.isEqualTo("/tmp/local-cv.sh");
|
||||||
|
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getStartupWaitMs())
|
||||||
|
.isEqualTo(250);
|
||||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isFalse();
|
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isFalse();
|
||||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory()).isEqualTo("/tmp/source");
|
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory()).isEqualTo("/tmp/source");
|
||||||
assertThat(properties.getEditing().getLocalDirector().getPollIntervalMs()).isEqualTo(9000);
|
assertThat(properties.getEditing().getLocalDirector().getPollIntervalMs()).isEqualTo(9000);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
package org.example.videoclips.editing;
|
||||||
|
|
||||||
|
import org.example.videoclips.config.VideoClippingProperties;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
class LocalCvWorkerProcessManagerTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotStartWhenProviderIsNotLocalCv() {
|
||||||
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
|
properties.getEditing().getVisualAnalysis().setProvider("heuristic");
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||||
|
|
||||||
|
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||||
|
throw new AssertionError("worker should not start");
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.start();
|
||||||
|
|
||||||
|
assertThat(manager.isRunning()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doesNotStartWhenAutoStartIsDisabled() {
|
||||||
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
|
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(false);
|
||||||
|
|
||||||
|
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||||
|
throw new AssertionError("worker should not start");
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.start();
|
||||||
|
|
||||||
|
assertThat(manager.isRunning()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsWorkerScriptWithEndpointHostAndPort() throws Exception {
|
||||||
|
Path script = tempDir.resolve("run-worker.sh");
|
||||||
|
Files.writeString(script, "#!/usr/bin/env bash\n");
|
||||||
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
|
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||||
|
properties.getEditing().getVisualAnalysis().setEndpoint("http://localhost:9005/v1/analyze-visuals");
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(script.toString());
|
||||||
|
AtomicReference<ProcessBuilder> captured = new AtomicReference<>();
|
||||||
|
TestProcess process = new TestProcess();
|
||||||
|
|
||||||
|
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||||
|
captured.set(processBuilder);
|
||||||
|
return process;
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.start();
|
||||||
|
|
||||||
|
assertThat(manager.isRunning()).isTrue();
|
||||||
|
assertThat(captured.get().command()).containsExactly(script.toAbsolutePath().normalize().toString());
|
||||||
|
assertThat(captured.get().environment()).containsEntry("LOCAL_CV_HOST", "localhost");
|
||||||
|
assertThat(captured.get().environment()).containsEntry("LOCAL_CV_PORT", "9005");
|
||||||
|
|
||||||
|
manager.stop();
|
||||||
|
|
||||||
|
assertThat(process.destroyed).isTrue();
|
||||||
|
assertThat(manager.isRunning()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsMissingWorkerScriptWhenAutoStartIsEnabled() {
|
||||||
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
|
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(tempDir.resolve("missing.sh").toString());
|
||||||
|
|
||||||
|
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||||
|
throw new AssertionError("worker should not start");
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThatThrownBy(manager::start)
|
||||||
|
.isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessageContaining("Local CV worker script does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resetsRunningStateWhenWorkerExitsDuringStartupWindow() throws Exception {
|
||||||
|
Path script = tempDir.resolve("run-worker.sh");
|
||||||
|
Files.writeString(script, "#!/usr/bin/env bash\n");
|
||||||
|
VideoClippingProperties properties = new VideoClippingProperties();
|
||||||
|
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(script.toString());
|
||||||
|
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setStartupWaitMs(1);
|
||||||
|
TestProcess process = new TestProcess();
|
||||||
|
process.alive = false;
|
||||||
|
|
||||||
|
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> process);
|
||||||
|
|
||||||
|
assertThatThrownBy(manager::start)
|
||||||
|
.isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessageContaining("Local CV worker exited during startup window");
|
||||||
|
assertThat(manager.isRunning()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestProcess extends Process {
|
||||||
|
private boolean alive = true;
|
||||||
|
private boolean destroyed;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
return OutputStream.nullOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return new ByteArrayInputStream("worker ready\n".getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getErrorStream() {
|
||||||
|
return InputStream.nullInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int waitFor() {
|
||||||
|
alive = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean waitFor(long timeout, TimeUnit unit) {
|
||||||
|
alive = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int exitValue() {
|
||||||
|
return alive ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
destroyed = true;
|
||||||
|
alive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Process destroyForcibly() {
|
||||||
|
destroyed = true;
|
||||||
|
alive = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlive() {
|
||||||
|
return alive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long pid() {
|
||||||
|
return 1234;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue