Add local CV worker readiness checks
This commit is contained in:
parent
80eca92d56
commit
adc979eea8
|
|
@ -36,3 +36,9 @@ build/
|
|||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### Local CV runtime ###
|
||||
.venv-local-cv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
yolov*.pt
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ video-clipping:
|
|||
timeout-ms: 30000
|
||||
fallback-to-heuristic: true
|
||||
local-cv-worker:
|
||||
auto-start: false
|
||||
auto-start: true
|
||||
script: ./tools/run_local_cv_worker.sh
|
||||
startup-wait-ms: 0
|
||||
startup-wait-ms: 120000
|
||||
health-path: /health
|
||||
health-check-interval-ms: 1000
|
||||
```
|
||||
|
||||
Equivalent environment variables:
|
||||
|
|
@ -43,7 +45,9 @@ VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS=30000
|
|||
VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC=true
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START=true
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_SCRIPT=./tools/run_local_cv_worker.sh
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS=0
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS=120000
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_HEALTH_PATH=/health
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_HEALTH_CHECK_INTERVAL_MS=1000
|
||||
```
|
||||
|
||||
This repository includes an optional starter worker:
|
||||
|
|
@ -68,6 +72,18 @@ Health check:
|
|||
curl http://127.0.0.1:8091/health
|
||||
```
|
||||
|
||||
## Preload Dependencies And Model
|
||||
|
||||
Do this once before starting the Spring service if you do not want dependency installation or YOLO model download to happen during application startup:
|
||||
|
||||
```bash
|
||||
LOCAL_CV_PRELOAD_ONLY=true tools/run_local_cv_worker.sh
|
||||
```
|
||||
|
||||
This creates `.venv-local-cv`, installs `tools/local_cv_requirements.txt`, and downloads/caches the configured YOLO model. After that, Spring-managed startup should be much faster.
|
||||
|
||||
Do not commit `.venv-local-cv`, Python wheels, Torch binaries, or YOLO weights into source control. They are large, platform-specific runtime artifacts. For production, prefer a prebuilt container image or a VM/bootstrap step that runs the preload command during deployment.
|
||||
|
||||
## Spring-Managed Worker Startup
|
||||
|
||||
The Spring application can start the local CV worker for local runs:
|
||||
|
|
@ -78,9 +94,9 @@ 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.
|
||||
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, polls the configured health endpoint until it returns HTTP 2xx, 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.
|
||||
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. Use the preload command or a prebuilt image to avoid that startup cost.
|
||||
|
||||
## Worker Request Contract
|
||||
|
||||
|
|
|
|||
|
|
@ -904,6 +904,11 @@ public class VideoClippingProperties {
|
|||
@Min(0)
|
||||
private long startupWaitMs = 0;
|
||||
|
||||
private String healthPath = "/health";
|
||||
|
||||
@Min(1)
|
||||
private long healthCheckIntervalMs = 1000;
|
||||
|
||||
public boolean isAutoStart() {
|
||||
return autoStart;
|
||||
}
|
||||
|
|
@ -927,6 +932,22 @@ public class VideoClippingProperties {
|
|||
public void setStartupWaitMs(long startupWaitMs) {
|
||||
this.startupWaitMs = startupWaitMs;
|
||||
}
|
||||
|
||||
public String getHealthPath() {
|
||||
return healthPath;
|
||||
}
|
||||
|
||||
public void setHealthPath(String healthPath) {
|
||||
this.healthPath = healthPath;
|
||||
}
|
||||
|
||||
public long getHealthCheckIntervalMs() {
|
||||
return healthCheckIntervalMs;
|
||||
}
|
||||
|
||||
public void setHealthCheckIntervalMs(long healthCheckIntervalMs) {
|
||||
this.healthCheckIntervalMs = healthCheckIntervalMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
|
@ -28,6 +31,7 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
|
||||
private final VideoClippingProperties.Editing.VisualAnalysis visualAnalysis;
|
||||
private final ProcessLauncher processLauncher;
|
||||
private final HealthChecker healthChecker;
|
||||
private Process process;
|
||||
private Thread outputThread;
|
||||
private Thread watcherThread;
|
||||
|
|
@ -36,15 +40,24 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
|
||||
@Autowired
|
||||
public LocalCvWorkerProcessManager(VideoClippingProperties properties) {
|
||||
this(properties, ProcessBuilder::start);
|
||||
this(properties, ProcessBuilder::start, new HttpHealthChecker());
|
||||
}
|
||||
|
||||
LocalCvWorkerProcessManager(
|
||||
VideoClippingProperties properties,
|
||||
ProcessLauncher processLauncher
|
||||
) {
|
||||
this(properties, processLauncher, new HttpHealthChecker());
|
||||
}
|
||||
|
||||
LocalCvWorkerProcessManager(
|
||||
VideoClippingProperties properties,
|
||||
ProcessLauncher processLauncher,
|
||||
HealthChecker healthChecker
|
||||
) {
|
||||
this.visualAnalysis = properties.getEditing().getVisualAnalysis();
|
||||
this.processLauncher = processLauncher;
|
||||
this.healthChecker = healthChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -63,8 +76,11 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
|
||||
VideoClippingProperties.Editing.VisualAnalysis.LocalCvWorker worker = visualAnalysis.getLocalCvWorker();
|
||||
Path script = Path.of(worker.getScript()).toAbsolutePath().normalize();
|
||||
log.info("event=local_cv_worker_starting script={} endpoint={} startup_wait_ms={}",
|
||||
script, visualAnalysis.getEndpoint(), worker.getStartupWaitMs());
|
||||
URI healthUri = healthUri(worker);
|
||||
log.info("event=local_cv_worker_starting script={} endpoint={} health_uri={} startup_wait_ms={} "
|
||||
+ "health_check_interval_ms={}",
|
||||
script, visualAnalysis.getEndpoint(), healthUri, worker.getStartupWaitMs(),
|
||||
worker.getHealthCheckIntervalMs());
|
||||
if (!Files.isRegularFile(script)) {
|
||||
throw new IllegalStateException("Local CV worker script does not exist: " + script);
|
||||
}
|
||||
|
|
@ -86,9 +102,9 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
watcherThread.start();
|
||||
log.info("event=local_cv_worker_started script={} pid={} host={} port={} endpoint={}",
|
||||
script, process.pid(), endpoint.host(), endpoint.port(), visualAnalysis.getEndpoint());
|
||||
waitForStartupWindow(worker.getStartupWaitMs());
|
||||
log.info("event=local_cv_worker_startup_window_completed script={} pid={} alive={}",
|
||||
script, process.pid(), process.isAlive());
|
||||
waitUntilHealthy(worker, healthUri);
|
||||
log.info("event=local_cv_worker_ready script={} pid={} health_uri={} alive={}",
|
||||
script, process.pid(), healthUri, process.isAlive());
|
||||
} catch (IOException ex) {
|
||||
running = false;
|
||||
log.error("event=local_cv_worker_start_failed script={} error_type={} message={}",
|
||||
|
|
@ -147,20 +163,41 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
&& visualAnalysis.getLocalCvWorker().isAutoStart();
|
||||
}
|
||||
|
||||
private void waitForStartupWindow(long startupWaitMs) {
|
||||
private void waitUntilHealthy(
|
||||
VideoClippingProperties.Editing.VisualAnalysis.LocalCvWorker worker,
|
||||
URI healthUri
|
||||
) {
|
||||
long startupWaitMs = worker.getStartupWaitMs();
|
||||
if (startupWaitMs <= 0) {
|
||||
log.info("event=local_cv_worker_startup_wait_skipped reason=disabled");
|
||||
log.info("event=local_cv_worker_health_wait_skipped reason=disabled health_uri={}", healthUri);
|
||||
return;
|
||||
}
|
||||
long startedAt = System.nanoTime();
|
||||
long deadline = System.currentTimeMillis() + startupWaitMs;
|
||||
long intervalMs = Math.max(1, worker.getHealthCheckIntervalMs());
|
||||
try {
|
||||
Thread.sleep(startupWaitMs);
|
||||
while (System.currentTimeMillis() <= deadline) {
|
||||
if (process != null && !process.isAlive()) {
|
||||
throw new IllegalStateException("Local CV worker exited before becoming healthy");
|
||||
}
|
||||
HealthResult result = healthChecker.check(healthUri, Math.min(intervalMs, 5000));
|
||||
log.info("event=local_cv_worker_health_check health_uri={} healthy={} status_code={} "
|
||||
+ "elapsed_ms={} message={}",
|
||||
healthUri, result.healthy(), result.statusCode(), elapsedMillis(startedAt),
|
||||
result.message());
|
||||
if (result.healthy()) {
|
||||
log.info("event=local_cv_worker_health_ready health_uri={} elapsed_ms={}",
|
||||
healthUri, elapsedMillis(startedAt));
|
||||
return;
|
||||
}
|
||||
Thread.sleep(intervalMs);
|
||||
}
|
||||
} 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");
|
||||
throw new IllegalStateException("Interrupted while waiting for local CV worker health", ex);
|
||||
}
|
||||
throw new IllegalStateException("Local CV worker did not become healthy within "
|
||||
+ startupWaitMs + "ms at " + healthUri);
|
||||
}
|
||||
|
||||
private void cleanupFailedStart() {
|
||||
|
|
@ -181,6 +218,28 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
return new Endpoint(host, port);
|
||||
}
|
||||
|
||||
private URI healthUri(VideoClippingProperties.Editing.VisualAnalysis.LocalCvWorker worker) {
|
||||
URI endpointUri = URI.create(visualAnalysis.getEndpoint());
|
||||
String scheme = endpointUri.getScheme() == null || endpointUri.getScheme().isBlank()
|
||||
? "http"
|
||||
: endpointUri.getScheme();
|
||||
String host = endpointUri.getHost() == null || endpointUri.getHost().isBlank()
|
||||
? "127.0.0.1"
|
||||
: endpointUri.getHost();
|
||||
int port = endpointUri.getPort();
|
||||
String healthPath = worker.getHealthPath() == null || worker.getHealthPath().isBlank()
|
||||
? "/health"
|
||||
: worker.getHealthPath();
|
||||
if (!healthPath.startsWith("/")) {
|
||||
healthPath = "/" + healthPath;
|
||||
}
|
||||
return URI.create("%s://%s%s%s".formatted(scheme, host, port < 0 ? "" : ":" + port, healthPath));
|
||||
}
|
||||
|
||||
private long elapsedMillis(long startedAt) {
|
||||
return (System.nanoTime() - startedAt) / 1_000_000;
|
||||
}
|
||||
|
||||
private Thread outputReader(InputStream inputStream, Path script) {
|
||||
Thread thread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
|
|
@ -228,4 +287,40 @@ public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
|||
interface ProcessLauncher {
|
||||
Process start(ProcessBuilder processBuilder) throws IOException;
|
||||
}
|
||||
|
||||
record HealthResult(boolean healthy, int statusCode, String message) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface HealthChecker {
|
||||
HealthResult check(URI healthUri, long timeoutMs) throws InterruptedException;
|
||||
}
|
||||
|
||||
private static class HttpHealthChecker implements HealthChecker {
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
|
||||
@Override
|
||||
public HealthResult check(URI healthUri, long timeoutMs) throws InterruptedException {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(healthUri)
|
||||
.timeout(Duration.ofMillis(timeoutMs))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return new HealthResult(response.statusCode() >= 200 && response.statusCode() < 300,
|
||||
response.statusCode(), preview(response.body()));
|
||||
} catch (IOException ex) {
|
||||
return new HealthResult(false, 0, ex.getClass().getSimpleName() + ": " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String preview(String body) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String compact = body.replaceAll("\\s+", " ").trim();
|
||||
return compact.length() <= 160 ? compact : compact.substring(0, 160) + "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ video-clipping:
|
|||
enabled: true
|
||||
local-director:
|
||||
enabled: true
|
||||
auto-render-when-plan-appears: false
|
||||
auto-render-when-plan-appears: true
|
||||
require-approval-before-render: true
|
||||
approval-file-name: approved.flag
|
||||
|
||||
|
|
|
|||
|
|
@ -54,9 +54,11 @@ video-clipping:
|
|||
timeout-ms: ${VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS:30000}
|
||||
fallback-to-heuristic: ${VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC:true}
|
||||
local-cv-worker:
|
||||
auto-start: ${VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START:false}
|
||||
auto-start: ${VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START:true}
|
||||
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}
|
||||
startup-wait-ms: ${VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS:120000}
|
||||
health-path: ${VIDEO_EDITING_LOCAL_CV_WORKER_HEALTH_PATH:/health}
|
||||
health-check-interval-ms: ${VIDEO_EDITING_LOCAL_CV_WORKER_HEALTH_CHECK_INTERVAL_MS:1000}
|
||||
local-director:
|
||||
enabled: ${VIDEO_EDITING_LOCAL_DIRECTOR_ENABLED:true}
|
||||
source-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_SOURCE_DIRECTORY:./input/editing/source}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ class VideoClippingPropertiesTest {
|
|||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getScript())
|
||||
.isEqualTo("./tools/run_local_cv_worker.sh");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getStartupWaitMs()).isZero();
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getHealthPath())
|
||||
.isEqualTo("/health");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getHealthCheckIntervalMs())
|
||||
.isEqualTo(1000);
|
||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isTrue();
|
||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory())
|
||||
.isEqualTo("./input/editing/source");
|
||||
|
|
@ -108,6 +112,8 @@ class VideoClippingPropertiesTest {
|
|||
"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.visual-analysis.local-cv-worker.health-path=/ready",
|
||||
"video-clipping.editing.visual-analysis.local-cv-worker.health-check-interval-ms=25",
|
||||
"video-clipping.editing.local-director.enabled=false",
|
||||
"video-clipping.editing.local-director.source-directory=/tmp/source",
|
||||
"video-clipping.editing.local-director.poll-interval-ms=9000",
|
||||
|
|
@ -152,6 +158,10 @@ class VideoClippingPropertiesTest {
|
|||
.isEqualTo("/tmp/local-cv.sh");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getStartupWaitMs())
|
||||
.isEqualTo(250);
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getHealthPath())
|
||||
.isEqualTo("/ready");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getHealthCheckIntervalMs())
|
||||
.isEqualTo(25);
|
||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isFalse();
|
||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory()).isEqualTo("/tmp/source");
|
||||
assertThat(properties.getEditing().getLocalDirector().getPollIntervalMs()).isEqualTo(9000);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
class CinematicEditingLocalProfileTest {
|
||||
|
||||
@Test
|
||||
void enablesLocalDirectorAndDisablesEightSecondFolderScheduler() {
|
||||
void enablesLocalDirectorAutoRenderAndDisablesEightSecondFolderScheduler() {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(new ClassPathResource("application-cinematic-editing-local.yml"));
|
||||
Properties properties = factory.getObject();
|
||||
|
|
@ -21,7 +21,7 @@ class CinematicEditingLocalProfileTest {
|
|||
assertThat(properties.getProperty("video-clipping.editing.enabled")).isEqualTo("true");
|
||||
assertThat(properties.getProperty("video-clipping.editing.local-director.enabled")).isEqualTo("true");
|
||||
assertThat(properties.getProperty("video-clipping.editing.local-director.auto-render-when-plan-appears"))
|
||||
.isEqualTo("false");
|
||||
.isEqualTo("true");
|
||||
assertThat(properties.getProperty("video-clipping.editing.local-director.require-approval-before-render"))
|
||||
.isEqualTo("true");
|
||||
assertThat(properties.getProperty("video-clipping.editing.local-director.approval-file-name"))
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import org.junit.jupiter.api.io.TempDir;
|
|||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -59,12 +61,20 @@ class LocalCvWorkerProcessManagerTest {
|
|||
properties.getEditing().getVisualAnalysis().setEndpoint("http://localhost:9005/v1/analyze-visuals");
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(script.toString());
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setStartupWaitMs(100);
|
||||
AtomicReference<ProcessBuilder> captured = new AtomicReference<>();
|
||||
AtomicReference<URI> healthUri = new AtomicReference<>();
|
||||
TestProcess process = new TestProcess();
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(
|
||||
properties,
|
||||
processBuilder -> {
|
||||
captured.set(processBuilder);
|
||||
return process;
|
||||
},
|
||||
(uri, timeoutMs) -> {
|
||||
healthUri.set(uri);
|
||||
return new LocalCvWorkerProcessManager.HealthResult(true, 200, "ok");
|
||||
});
|
||||
|
||||
manager.start();
|
||||
|
|
@ -73,6 +83,7 @@ class LocalCvWorkerProcessManagerTest {
|
|||
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");
|
||||
assertThat(healthUri.get()).hasToString("http://localhost:9005/health");
|
||||
|
||||
manager.stop();
|
||||
|
||||
|
|
@ -97,7 +108,7 @@ class LocalCvWorkerProcessManagerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void resetsRunningStateWhenWorkerExitsDuringStartupWindow() throws Exception {
|
||||
void resetsRunningStateWhenWorkerExitsBeforeHealthCheckPasses() throws Exception {
|
||||
Path script = tempDir.resolve("run-worker.sh");
|
||||
Files.writeString(script, "#!/usr/bin/env bash\n");
|
||||
VideoClippingProperties properties = new VideoClippingProperties();
|
||||
|
|
@ -108,14 +119,69 @@ class LocalCvWorkerProcessManagerTest {
|
|||
TestProcess process = new TestProcess();
|
||||
process.alive = false;
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> process);
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(
|
||||
properties,
|
||||
processBuilder -> process,
|
||||
(uri, timeoutMs) -> new LocalCvWorkerProcessManager.HealthResult(false, 0, "connection refused"));
|
||||
|
||||
assertThatThrownBy(manager::start)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Local CV worker exited during startup window");
|
||||
.hasMessageContaining("Local CV worker exited before becoming healthy");
|
||||
assertThat(manager.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWhenWorkerDoesNotBecomeHealthyBeforeTimeout() 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);
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setHealthCheckIntervalMs(1);
|
||||
TestProcess process = new TestProcess();
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(
|
||||
properties,
|
||||
processBuilder -> process,
|
||||
(uri, timeoutMs) -> new LocalCvWorkerProcessManager.HealthResult(false, 503, "starting"));
|
||||
|
||||
assertThatThrownBy(manager::start)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Local CV worker did not become healthy");
|
||||
assertThat(process.destroyed).isTrue();
|
||||
assertThat(manager.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void retriesHealthChecksUntilWorkerIsReady() 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(100);
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setHealthCheckIntervalMs(1);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
TestProcess process = new TestProcess();
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(
|
||||
properties,
|
||||
processBuilder -> process,
|
||||
(uri, timeoutMs) -> attempts.incrementAndGet() < 2
|
||||
? new LocalCvWorkerProcessManager.HealthResult(false, 0, "connection refused")
|
||||
: new LocalCvWorkerProcessManager.HealthResult(true, 200, "ok"));
|
||||
|
||||
manager.start();
|
||||
|
||||
assertThat(attempts.get()).isEqualTo(2);
|
||||
assertThat(manager.isRunning()).isTrue();
|
||||
|
||||
manager.stop();
|
||||
}
|
||||
|
||||
private static class TestProcess extends Process {
|
||||
private boolean alive = true;
|
||||
private boolean destroyed;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
video-clipping.folder-scheduler.enabled=false
|
||||
video-clipping.editing.local-director.enabled=false
|
||||
video-clipping.editing.highlight-scheduler.enabled=false
|
||||
video-clipping.editing.visual-analysis.local-cv-worker.auto-start=false
|
||||
|
|
@ -3,6 +3,7 @@ set -euo pipefail
|
|||
|
||||
HOST="${LOCAL_CV_HOST:-127.0.0.1}"
|
||||
PORT="${LOCAL_CV_PORT:-8091}"
|
||||
MODEL="${LOCAL_CV_YOLO_MODEL:-yolov8n.pt}"
|
||||
|
||||
if [ ! -d ".venv-local-cv" ]; then
|
||||
python3 -m venv .venv-local-cv
|
||||
|
|
@ -12,4 +13,13 @@ fi
|
|||
python -m pip install --upgrade pip
|
||||
python -m pip install -r tools/local_cv_requirements.txt
|
||||
|
||||
if [ "${LOCAL_CV_DISABLE_YOLO:-false}" != "true" ]; then
|
||||
LOCAL_CV_YOLO_MODEL="$MODEL" python -c "import os; from ultralytics import YOLO; YOLO(os.environ['LOCAL_CV_YOLO_MODEL'])"
|
||||
fi
|
||||
|
||||
if [ "${LOCAL_CV_PRELOAD_ONLY:-false}" = "true" ]; then
|
||||
echo "Local CV dependencies and model are ready."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec uvicorn tools.local_cv_worker:app --host "$HOST" --port "$PORT"
|
||||
|
|
|
|||
Loading…
Reference in New Issue