openvidu-test-e2e: do not use a shared cache between test runs for server SDK apps

pull/918/head
pabloFuente 2026-10-21 16:33:37 +02:00
parent cf1512c108
commit 7b8b072005
2 changed files with 56 additions and 9 deletions

View File

@ -16,6 +16,7 @@ import java.nio.file.Paths;
import java.time.Duration; import java.time.Duration;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -43,6 +44,9 @@ import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerImageName;
import com.github.dockerjava.api.model.Mount;
import com.github.dockerjava.api.model.MountType;
import io.livekit.server.AccessToken; import io.livekit.server.AccessToken;
import io.livekit.server.CanPublish; import io.livekit.server.CanPublish;
import io.livekit.server.IngressServiceClient; import io.livekit.server.IngressServiceClient;
@ -470,6 +474,34 @@ public class OpenViduTestE2e {
startServerSdkPublisher(sdk, roomName, codec, 1); startServerSdkPublisher(sdk, roomName, codec, 1);
} }
// Dependency cache volumes of the server SDK publishers, one per SDK, created
// on first use and alive only for this JVM.
private static final Map<String, String> SDK_CACHE_VOLUMES = new ConcurrentHashMap<>();
/**
* Name of the dependency cache volume of the given SDK for this JVM, creating
* it on first use. The volume carries the Testcontainers session labels, so
* Ryuk removes it when the JVM ends, the same way it removes the containers;
* a shutdown hook removes it too for the runs that disable Ryuk.
*/
private static String sdkCacheVolume(String sdk) {
return SDK_CACHE_VOLUMES.computeIfAbsent(sdk, s -> {
String name = "openvidu-e2e-sdk-cache-" + s + "-" + DockerClientFactory.SESSION_ID;
Map<String, String> labels = new HashMap<>(DockerClientFactory.DEFAULT_LABELS);
labels.put(DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL, DockerClientFactory.SESSION_ID);
DockerClientFactory.instance().client().createVolumeCmd().withName(name).withLabels(labels).exec();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
DockerClientFactory.instance().client().removeVolumeCmd(name).exec();
} catch (Exception e) {
// Already removed by Ryuk, or still used by a container Ryuk is
// about to remove together with the volume
}
}));
return name;
});
}
/** /**
* Starts a LiveKit server RTC SDK participant (a minimal program at * Starts a LiveKit server RTC SDK participant (a minimal program at
* src/test/resources/&lt;sdk&gt;-publisher) in a plain runtime container * src/test/resources/&lt;sdk&gt;-publisher) in a plain runtime container
@ -478,8 +510,11 @@ public class OpenViduTestE2e {
* simulcast for VP8/H264 and without SVC (scalabilityMode L1T1) for VP9/AV1. * simulcast for VP8/H264 and without SVC (scalabilityMode L1T1) for VP9/AV1.
* *
* The container mounts the program read-only at /app, copies it to /work and * The container mounts the program read-only at /app, copies it to /work and
* runs it there, with a per-SDK dependency cache mounted at /cache so * runs it there. /cache is a Docker volume that exists only for the current
* repeated runs skip downloads. Returns once the program logs * test JVM (see {@link #sdkCacheVolume}): nothing of a previous run or of
* another job on the same machine reaches the program, while the publishers
* of one SDK still share their downloads (and the rust build, which takes up
* to 30 minutes from cold) within the run. Returns once the program logs
* TRACK_PUBLISHED. The container is stopped automatically on test dispose. * TRACK_PUBLISHED. The container is stopped automatically on test dispose.
*/ */
public void startServerSdkPublisher(String sdk, String roomName, String codec, int layers) throws Exception { public void startServerSdkPublisher(String sdk, String roomName, String codec, int layers) throws Exception {
@ -542,14 +577,21 @@ public class OpenViduTestE2e {
env.put("VIDEO_LAYERS", String.valueOf(layers)); env.put("VIDEO_LAYERS", String.valueOf(layers));
String programDir = Paths.get("src/test/resources/" + sdk + "-publisher").toAbsolutePath().toString(); String programDir = Paths.get("src/test/resources/" + sdk + "-publisher").toAbsolutePath().toString();
Path cacheDir = Paths.get(System.getProperty("java.io.tmpdir"), "openvidu-e2e-sdk-cache", sdk); String cacheVolume = sdkCacheVolume(sdk);
Files.createDirectories(cacheDir);
GenericContainer<?> publisherContainer = new GenericContainer<>(DockerImageName.parse(image)) GenericContainer<?> publisherContainer = new GenericContainer<>(DockerImageName.parse(image))
.withCreateContainerCmdModifier( .withCreateContainerCmdModifier(
cmd -> cmd.withName(sdk + "-publisher-" + (int) (Math.random() * 100000))) cmd -> cmd.withName(sdk + "-publisher-" + (int) (Math.random() * 100000)))
.withNetworkMode("host").withFileSystemBind(programDir, "/app", BindMode.READ_ONLY) // Not withFileSystemBind: it resolves its first argument as a host path
.withFileSystemBind(cacheDir.toString(), "/cache").withEnv(env) .withCreateContainerCmdModifier(cmd -> {
List<Mount> mounts = new ArrayList<>();
if (cmd.getHostConfig().getMounts() != null) {
mounts.addAll(cmd.getHostConfig().getMounts());
}
mounts.add(new Mount().withType(MountType.VOLUME).withSource(cacheVolume).withTarget("/cache"));
cmd.getHostConfig().withMounts(mounts);
})
.withNetworkMode("host").withFileSystemBind(programDir, "/app", BindMode.READ_ONLY).withEnv(env)
.withCommand("sh", "-c", "mkdir -p /work && cp -r /app/. /work && cd /work && " + runCommand) .withCommand("sh", "-c", "mkdir -p /work && cp -r /app/. /work && cd /work && " + runCommand)
.withLogConsumer( .withLogConsumer(
frame -> log.info("[{}-publisher] {}", sdk, frame.getUtf8String().stripTrailing())) frame -> log.info("[{}-publisher] {}", sdk, frame.getUtf8String().stripTrailing()))

View File

@ -12,9 +12,14 @@ Minimal LiveKit server RTC SDK publisher programs used by
| `dotnet-publisher/` | `Livekit.Rtc.Dotnet` | synthetic frames (FFI) | | `dotnet-publisher/` | `Livekit.Rtc.Dotnet` | synthetic frames (FFI) |
Each program is launched by `OpenViduTestE2e#startServerSdkPublisher` inside a Each program is launched by `OpenViduTestE2e#startServerSdkPublisher` inside a
plain runtime container of its language (see that method for the images, run plain runtime container of its language (see that method for the images and run
commands and per-SDK dependency caches). The directory is mounted read-only at commands). The directory is mounted read-only at `/app`, copied to `/work` and
`/app`, copied to `/work` and run there. The program must print the exact line run there. The dependency cache at `/cache` is a Docker volume that lives only
for the test JVM that created it (one per SDK, removed with the containers when
the JVM ends), so a run never sees the downloads, builds or caches of a previous
run or of another job on the same machine; within a run the publishers of an SDK
share it, which keeps the Rust publisher's libwebrtc build to one per run. The
program must print the exact line
`TRACK_PUBLISHED` once the track is published: the Java test waits for it before `TRACK_PUBLISHED` once the track is published: the Java test waits for it before
asserting, and stops the container when the test ends, so the programs never asserting, and stops the container when the test ends, so the programs never
exit on their own. exit on their own.