|
| 1 | +package org.togetherjava.jshellapi.service; |
| 2 | + |
| 3 | +import com.github.dockerjava.api.DockerClient; |
| 4 | +import com.github.dockerjava.api.async.ResultCallback; |
| 5 | +import com.github.dockerjava.api.command.PullImageResultCallback; |
| 6 | +import com.github.dockerjava.api.model.*; |
| 7 | +import com.github.dockerjava.core.DefaultDockerClientConfig; |
| 8 | +import com.github.dockerjava.core.DockerClientImpl; |
| 9 | +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; |
| 10 | +import org.slf4j.Logger; |
| 11 | +import org.slf4j.LoggerFactory; |
| 12 | +import org.springframework.beans.factory.DisposableBean; |
| 13 | +import org.springframework.beans.factory.annotation.Autowired; |
| 14 | +import org.springframework.stereotype.Service; |
| 15 | +import org.togetherjava.jshellapi.Config; |
| 16 | + |
| 17 | +import java.io.*; |
| 18 | +import java.nio.charset.StandardCharsets; |
| 19 | +import java.time.Duration; |
| 20 | +import java.util.*; |
| 21 | +import java.util.concurrent.TimeUnit; |
| 22 | + |
| 23 | +@Service |
| 24 | +public class DockerService implements DisposableBean { |
| 25 | + private static final Logger LOGGER = LoggerFactory.getLogger(DockerService.class); |
| 26 | + private static final String WORKER_LABEL = "jshell-api-worker"; |
| 27 | + private static final UUID WORKER_UNIQUE_ID = UUID.randomUUID(); |
| 28 | + |
| 29 | + private final DockerClient client; |
| 30 | + |
| 31 | + public DockerService(Config config) { |
| 32 | + DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); |
| 33 | + ApacheDockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() |
| 34 | + .dockerHost(clientConfig.getDockerHost()) |
| 35 | + .sslConfig(clientConfig.getSSLConfig()) |
| 36 | + .responseTimeout(Duration.ofSeconds(config.dockerResponseTimeout())) |
| 37 | + .connectionTimeout(Duration.ofSeconds(config.dockerConnectionTimeout())) |
| 38 | + .build(); |
| 39 | + this.client = DockerClientImpl.getInstance(clientConfig, httpClient); |
| 40 | + |
| 41 | + cleanupLeftovers(WORKER_UNIQUE_ID); |
| 42 | + } |
| 43 | + |
| 44 | + private void cleanupLeftovers(UUID currentId) { |
| 45 | + for (Container container : client.listContainersCmd().withLabelFilter(Set.of(WORKER_LABEL)).exec()) { |
| 46 | + String containerHumanName = container.getId() + " " + Arrays.toString(container.getNames()); |
| 47 | + LOGGER.info("Found worker container '{}'", containerHumanName); |
| 48 | + if (!container.getLabels().get(WORKER_LABEL).equals(currentId.toString())) { |
| 49 | + LOGGER.info("Killing container '{}'", containerHumanName); |
| 50 | + client.killContainerCmd(container.getId()).exec(); |
| 51 | + } |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + public String spawnContainer( |
| 56 | + long maxMemoryMegs, long cpus, String name, Duration evalTimeout, long sysoutLimit |
| 57 | + ) throws InterruptedException { |
| 58 | + String imageName = "togetherjava.org:5001/togetherjava/jshellwrapper"; |
| 59 | + boolean presentLocally = client.listImagesCmd() |
| 60 | + .withFilter("reference", List.of(imageName)) |
| 61 | + .exec() |
| 62 | + .stream() |
| 63 | + .flatMap(it -> Arrays.stream(it.getRepoTags())) |
| 64 | + .anyMatch(it -> it.endsWith(":master")); |
| 65 | + |
| 66 | + if (!presentLocally) { |
| 67 | + client.pullImageCmd(imageName) |
| 68 | + .withTag("master") |
| 69 | + .exec(new PullImageResultCallback()) |
| 70 | + .awaitCompletion(5, TimeUnit.MINUTES); |
| 71 | + } |
| 72 | + |
| 73 | + return client.createContainerCmd( |
| 74 | + imageName + ":master" |
| 75 | + ) |
| 76 | + .withHostConfig( |
| 77 | + HostConfig.newHostConfig() |
| 78 | + .withAutoRemove(true) |
| 79 | + .withInit(true) |
| 80 | + .withCapDrop(Capability.ALL) |
| 81 | + .withNetworkMode("none") |
| 82 | + .withPidsLimit(2000L) |
| 83 | + .withReadonlyRootfs(true) |
| 84 | + .withMemory(maxMemoryMegs * 1024 * 1024) |
| 85 | + .withCpuCount(cpus) |
| 86 | + ) |
| 87 | + .withStdinOpen(true) |
| 88 | + .withAttachStdin(true) |
| 89 | + .withAttachStderr(true) |
| 90 | + .withAttachStdout(true) |
| 91 | + .withEnv("evalTimeoutSeconds=" + evalTimeout.toSeconds(), "sysOutCharLimit=" + sysoutLimit) |
| 92 | + .withLabels(Map.of(WORKER_LABEL, WORKER_UNIQUE_ID.toString())) |
| 93 | + .withName(name) |
| 94 | + .exec() |
| 95 | + .getId(); |
| 96 | + } |
| 97 | + |
| 98 | + public InputStream startAndAttachToContainer(String containerId, InputStream stdin) throws IOException { |
| 99 | + PipedInputStream pipeIn = new PipedInputStream(); |
| 100 | + PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); |
| 101 | + |
| 102 | + client.attachContainerCmd(containerId) |
| 103 | + .withLogs(true) |
| 104 | + .withFollowStream(true) |
| 105 | + .withStdOut(true) |
| 106 | + .withStdErr(true) |
| 107 | + .withStdIn(stdin) |
| 108 | + .exec(new ResultCallback.Adapter<>() { |
| 109 | + @Override |
| 110 | + public void onNext(Frame object) { |
| 111 | + try { |
| 112 | + String payloadString = new String(object.getPayload(), StandardCharsets.UTF_8); |
| 113 | + if (object.getStreamType() == StreamType.STDOUT) { |
| 114 | + pipeOut.write(object.getPayload()); |
| 115 | + } else { |
| 116 | + LOGGER.warn( |
| 117 | + "Received STDERR from container {}: {}", |
| 118 | + containerId, |
| 119 | + payloadString |
| 120 | + ); |
| 121 | + } |
| 122 | + } catch (IOException e) { |
| 123 | + throw new UncheckedIOException(e); |
| 124 | + } |
| 125 | + } |
| 126 | + }); |
| 127 | + |
| 128 | + client.startContainerCmd(containerId).exec(); |
| 129 | + return pipeIn; |
| 130 | + } |
| 131 | + |
| 132 | + public void killContainerByName(String name) { |
| 133 | + for (Container container : client.listContainersCmd().withNameFilter(Set.of(name)).exec()) { |
| 134 | + client.killContainerCmd(container.getId()).exec(); |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + public boolean isDead(String containerName) { |
| 139 | + return client.listContainersCmd().withNameFilter(Set.of(containerName)).exec().isEmpty(); |
| 140 | + } |
| 141 | + |
| 142 | + @Override |
| 143 | + public void destroy() throws Exception { |
| 144 | + LOGGER.info("destroy() called. Destroying all containers..."); |
| 145 | + cleanupLeftovers(UUID.randomUUID()); |
| 146 | + client.close(); |
| 147 | + } |
| 148 | + |
| 149 | +} |
0 commit comments