mirror of https://github.com/OpenVidu/openvidu.git
openvidu-test-e2e: build server SDK publishers before the room exists
OpenVidu in evaluation mode closes a room after 5 minutes. The first test of each SDK created its room and only then pulled the image and built the publisher: for rust the room had been open for up to 344 s when it started publishing, and the room closed while the late subscribers were being checked. prepareServerSdkPublisher() now pulls and builds each SDK's publisher once per run, before the test creates any room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>dependabot/npm_and_yarn/openvidu-dashboard/minor-and-patch-ddfd6a8e82
parent
cbd73a3695
commit
1e3be671e0
|
|
@ -26,6 +26,7 @@ import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
@ -44,6 +45,7 @@ import org.testcontainers.DockerClientFactory;
|
||||||
import org.testcontainers.containers.BindMode;
|
import org.testcontainers.containers.BindMode;
|
||||||
import org.testcontainers.containers.GenericContainer;
|
import org.testcontainers.containers.GenericContainer;
|
||||||
import org.testcontainers.containers.Network;
|
import org.testcontainers.containers.Network;
|
||||||
|
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
|
||||||
import org.testcontainers.containers.wait.strategy.Wait;
|
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;
|
||||||
|
|
@ -519,6 +521,108 @@ public class OpenViduTestE2e {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the publisher program of a server SDK is built and run: its runtime
|
||||||
|
* image, the command that installs its toolchain and dependencies and compiles
|
||||||
|
* it into the /cache volume, the command that runs it (which still does both,
|
||||||
|
* from the cache), how many minutes each may take and the environment of both.
|
||||||
|
*/
|
||||||
|
private record SdkPublisher(String image, String prepareCommand, String runCommand, int timeoutMinutes,
|
||||||
|
Map<String, String> env) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SdkPublisher sdkPublisher(String sdk) {
|
||||||
|
Map<String, String> env = new HashMap<>();
|
||||||
|
switch (sdk) {
|
||||||
|
case "go":
|
||||||
|
env.put("GOMODCACHE", "/cache/gomod");
|
||||||
|
env.put("GOCACHE", "/cache/gobuild");
|
||||||
|
return new SdkPublisher("golang:1.27", "go build -o /dev/null .", "go run .", 5, env);
|
||||||
|
case "node":
|
||||||
|
env.put("npm_config_cache", "/cache/npm");
|
||||||
|
String npmInstall = "npm install --no-audit --no-fund --loglevel=error";
|
||||||
|
return new SdkPublisher("node:24", npmInstall, npmInstall + " && node main.mjs", 5, env);
|
||||||
|
case "python":
|
||||||
|
env.put("PIP_CACHE_DIR", "/cache/pip");
|
||||||
|
String pipInstall = "pip install -q -r requirements.txt";
|
||||||
|
return new SdkPublisher("python:3.14", pipInstall, pipInstall + " && python main.py", 5, env);
|
||||||
|
case "rust":
|
||||||
|
// webrtc-sys links a prebuilt libwebrtc that requires clang++ >= 21
|
||||||
|
// (apt.llvm.org); the first build compiles the whole livekit crate
|
||||||
|
// graph
|
||||||
|
String clang = "apt-get update -qq >/dev/null && apt-get install -y -qq lsb-release gnupg >/dev/null"
|
||||||
|
+ " && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && bash /tmp/llvm.sh 21 >/dev/null 2>&1";
|
||||||
|
env.put("CARGO_HOME", "/cache/cargo");
|
||||||
|
env.put("CARGO_TARGET_DIR", "/cache/cargo-target");
|
||||||
|
env.put("CC", "clang-21");
|
||||||
|
env.put("CXX", "clang++-21");
|
||||||
|
return new SdkPublisher("rust:1", clang + " && cargo build --release --locked",
|
||||||
|
clang + " && cargo run --release --locked", 30, env);
|
||||||
|
case "dotnet":
|
||||||
|
env.put("NUGET_PACKAGES", "/cache/nuget");
|
||||||
|
env.put("DOTNET_CLI_TELEMETRY_OPTOUT", "1");
|
||||||
|
return new SdkPublisher("mcr.microsoft.com/dotnet/sdk:10.0", "dotnet build -c Release",
|
||||||
|
"dotnet run -c Release", 10, env);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown server SDK publisher: " + sdk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container of the given SDK's image that mounts its publisher program
|
||||||
|
* read-only at /app and the SDK's cache volume at /cache, copies the program to
|
||||||
|
* /work and runs the command there. Its output is logged as [<sdk>-<role>].
|
||||||
|
*/
|
||||||
|
private GenericContainer<?> sdkPublisherContainer(String sdk, String role, String image, Map<String, String> env,
|
||||||
|
String command) {
|
||||||
|
String programDir = Paths.get("src/test/resources/" + sdk + "-publisher").toAbsolutePath().toString();
|
||||||
|
String cacheVolume = sdkCacheVolume(sdk);
|
||||||
|
return new GenericContainer<>(DockerImageName.parse(image))
|
||||||
|
.withCreateContainerCmdModifier(
|
||||||
|
cmd -> cmd.withName(sdk + "-" + role + "-" + (int) (Math.random() * 100000)))
|
||||||
|
// Not withFileSystemBind: it resolves its first argument as a host path
|
||||||
|
.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 && " + command)
|
||||||
|
.withLogConsumer(frame -> log.info("[{}-{}] {}", sdk, role, frame.getUtf8String().stripTrailing()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server SDKs whose publisher program is already prepared in this JVM
|
||||||
|
private static final Set<String> PREPARED_SDK_PUBLISHERS = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pulls the image of the given SDK's publisher program and runs its prepare
|
||||||
|
* command (toolchain, dependencies and compilation into the SDK's cache volume),
|
||||||
|
* once per JVM. Call it before creating the room the publisher will join: the
|
||||||
|
* first preparation of an SDK takes minutes (rust: image pull, clang install and
|
||||||
|
* the build of the whole livekit crate graph), and OpenVidu in evaluation mode
|
||||||
|
* closes every room after 5 minutes. Fails if the command does not exit with 0.
|
||||||
|
*/
|
||||||
|
public void prepareServerSdkPublisher(String sdk) {
|
||||||
|
synchronized (PREPARED_SDK_PUBLISHERS) {
|
||||||
|
if (PREPARED_SDK_PUBLISHERS.contains(sdk)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SdkPublisher publisher = sdkPublisher(sdk);
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
try (GenericContainer<?> container = sdkPublisherContainer(sdk, "prepare", publisher.image(),
|
||||||
|
publisher.env(), publisher.prepareCommand())
|
||||||
|
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()
|
||||||
|
.withTimeout(Duration.ofMinutes(publisher.timeoutMinutes())))) {
|
||||||
|
container.start();
|
||||||
|
}
|
||||||
|
log.info("{} SDK publisher prepared in {} s", sdk, (System.currentTimeMillis() - start) / 1000);
|
||||||
|
PREPARED_SDK_PUBLISHERS.add(sdk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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/<sdk>-publisher) in a plain runtime container
|
* src/test/resources/<sdk>-publisher) in a plain runtime container
|
||||||
|
|
@ -531,58 +635,17 @@ public class OpenViduTestE2e {
|
||||||
* test JVM (see {@link #sdkCacheVolume}): nothing of a previous run or of
|
* test JVM (see {@link #sdkCacheVolume}): nothing of a previous run or of
|
||||||
* another job on the same machine reaches the program, while the publishers
|
* 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
|
* 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
|
* to 30 minutes from cold) within the run. Run
|
||||||
* TRACK_PUBLISHED. The container is stopped automatically on test dispose.
|
* {@link #prepareServerSdkPublisher} before creating the room, so that the
|
||||||
|
* room does not wait for that. Returns once the program logs 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 {
|
||||||
if (layers < 1 || layers > 3) {
|
if (layers < 1 || layers > 3) {
|
||||||
throw new IllegalArgumentException("Unsupported number of video layers: " + layers);
|
throw new IllegalArgumentException("Unsupported number of video layers: " + layers);
|
||||||
}
|
}
|
||||||
final String image;
|
SdkPublisher publisher = sdkPublisher(sdk);
|
||||||
final String runCommand;
|
Map<String, String> env = new HashMap<>(publisher.env());
|
||||||
int startupTimeoutMinutes = 5;
|
|
||||||
Map<String, String> env = new HashMap<>();
|
|
||||||
switch (sdk) {
|
|
||||||
case "go":
|
|
||||||
image = "golang:1.27";
|
|
||||||
runCommand = "go run .";
|
|
||||||
env.put("GOMODCACHE", "/cache/gomod");
|
|
||||||
env.put("GOCACHE", "/cache/gobuild");
|
|
||||||
break;
|
|
||||||
case "node":
|
|
||||||
image = "node:24";
|
|
||||||
runCommand = "npm install --no-audit --no-fund --loglevel=error && node main.mjs";
|
|
||||||
env.put("npm_config_cache", "/cache/npm");
|
|
||||||
break;
|
|
||||||
case "python":
|
|
||||||
image = "python:3.14";
|
|
||||||
runCommand = "pip install -q -r requirements.txt && python main.py";
|
|
||||||
env.put("PIP_CACHE_DIR", "/cache/pip");
|
|
||||||
break;
|
|
||||||
case "rust":
|
|
||||||
image = "rust:1";
|
|
||||||
// webrtc-sys links a prebuilt libwebrtc that requires clang++ >= 21
|
|
||||||
// (apt.llvm.org); the first run then compiles the whole livekit
|
|
||||||
// crate graph
|
|
||||||
runCommand = "apt-get update -qq >/dev/null && apt-get install -y -qq lsb-release gnupg >/dev/null"
|
|
||||||
+ " && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && bash /tmp/llvm.sh 21 >/dev/null 2>&1"
|
|
||||||
+ " && cargo run --release --locked";
|
|
||||||
env.put("CARGO_HOME", "/cache/cargo");
|
|
||||||
env.put("CARGO_TARGET_DIR", "/cache/cargo-target");
|
|
||||||
env.put("CC", "clang-21");
|
|
||||||
env.put("CXX", "clang++-21");
|
|
||||||
startupTimeoutMinutes = 30;
|
|
||||||
break;
|
|
||||||
case "dotnet":
|
|
||||||
image = "mcr.microsoft.com/dotnet/sdk:10.0";
|
|
||||||
runCommand = "dotnet run -c Release";
|
|
||||||
env.put("NUGET_PACKAGES", "/cache/nuget");
|
|
||||||
env.put("DOTNET_CLI_TELEMETRY_OPTOUT", "1");
|
|
||||||
startupTimeoutMinutes = 10;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unknown server SDK publisher: " + sdk);
|
|
||||||
}
|
|
||||||
// Publishers receive a ready-made token: it keeps the programs shorter
|
// Publishers receive a ready-made token: it keeps the programs shorter
|
||||||
// (no per-SDK token dependency) and works with any api secret length
|
// (no per-SDK token dependency) and works with any api secret length
|
||||||
AccessToken accessToken = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
AccessToken accessToken = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||||
|
|
@ -593,27 +656,10 @@ public class OpenViduTestE2e {
|
||||||
env.put("VIDEO_CODEC", codec);
|
env.put("VIDEO_CODEC", codec);
|
||||||
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();
|
GenericContainer<?> publisherContainer = sdkPublisherContainer(sdk, "publisher", publisher.image(), env,
|
||||||
String cacheVolume = sdkCacheVolume(sdk);
|
publisher.runCommand())
|
||||||
|
|
||||||
GenericContainer<?> publisherContainer = new GenericContainer<>(DockerImageName.parse(image))
|
|
||||||
.withCreateContainerCmdModifier(
|
|
||||||
cmd -> cmd.withName(sdk + "-publisher-" + (int) (Math.random() * 100000)))
|
|
||||||
// Not withFileSystemBind: it resolves its first argument as a host path
|
|
||||||
.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)
|
|
||||||
.withLogConsumer(
|
|
||||||
frame -> log.info("[{}-publisher] {}", sdk, frame.getUtf8String().stripTrailing()))
|
|
||||||
.waitingFor(Wait.forLogMessage("^.*TRACK_PUBLISHED.*$", 1)
|
.waitingFor(Wait.forLogMessage("^.*TRACK_PUBLISHED.*$", 1)
|
||||||
.withStartupTimeout(Duration.ofMinutes(startupTimeoutMinutes)));
|
.withStartupTimeout(Duration.ofMinutes(publisher.timeoutMinutes())));
|
||||||
|
|
||||||
if ("go".equals(sdk)) {
|
if ("go".equals(sdk)) {
|
||||||
// The Go SDK sends pre-encoded media: loop a real (decodable) 5s
|
// The Go SDK sends pre-encoded media: loop a real (decodable) 5s
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,11 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
||||||
log.info("{} SDK {} {}-layer publisher to Chrome and Firefox early and late subscribers", sdk, codec,
|
log.info("{} SDK {} {}-layer publisher to Chrome and Firefox early and late subscribers", sdk, codec,
|
||||||
layers);
|
layers);
|
||||||
|
|
||||||
|
// Before the room exists: the first test of an SDK would otherwise spend the
|
||||||
|
// image pull and the build (minutes for rust) inside the room, which OpenVidu
|
||||||
|
// in evaluation mode closes after 5 minutes
|
||||||
|
this.prepareServerSdkPublisher(sdk);
|
||||||
|
|
||||||
OpenViduTestappUser chrome = setupBrowserAndConnectToOpenViduTestapp("chrome");
|
OpenViduTestappUser chrome = setupBrowserAndConnectToOpenViduTestapp("chrome");
|
||||||
OpenViduTestappUser firefox = setupBrowserAndConnectToOpenViduTestapp("firefox");
|
OpenViduTestappUser firefox = setupBrowserAndConnectToOpenViduTestapp("firefox");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue