Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.hyperfoil.clustering.ControllerServer Maven / Gradle / Ivy
package io.hyperfoil.clustering;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import io.hyperfoil.api.config.BenchmarkData;
import io.hyperfoil.api.config.BenchmarkDefinitionException;
import io.hyperfoil.api.config.Phase;
import io.hyperfoil.client.Client;
import io.hyperfoil.api.Version;
import io.hyperfoil.core.util.LowHigh;
import io.hyperfoil.core.util.Util;
import io.hyperfoil.internal.Properties;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.hyperfoil.api.config.Benchmark;
import io.hyperfoil.core.parser.BenchmarkParser;
import io.hyperfoil.core.parser.ParserException;
import io.vertx.core.AsyncResult;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.impl.NoStackTraceThrowable;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.FileUpload;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
class ControllerServer {
private static final Logger log = LoggerFactory.getLogger(ControllerServer.class);
private static final String MIME_TYPE_SERIALIZED = "application/java-serialized-object";
private static final String MIME_TYPE_MULTIPART = "multipart/form-data";
private static final Set MIME_TYPE_YAML = new HashSet<>(
Arrays.asList("text/vnd.yaml", "text/yaml", "text/x-yaml", "application/x-yaml"));
private static final String MIME_TYPE_JSON = "application/json";
private static final String MIME_TYPE_CSV = "text/csv";
private static final String CONTROLLER_HOST = Properties.get(Properties.CONTROLLER_HOST, "localhost");
private static final int CONTROLLER_PORT = Properties.getInt(Properties.CONTROLLER_PORT, 8090);
private static final Comparator PHASE_COMPARATOR =
Comparator.comparing(ControllerPhase::absoluteStartTime).thenComparing(p -> p.definition().name);
private static final String TRIGGER_URL = System.getProperty(Properties.TRIGGER_URL);
private final ControllerVerticle controller;
final HttpServer httpServer;
private final Router router;
private String baseURL;
ControllerServer(ControllerVerticle controller, Handler> handler) {
this.controller = controller;
router = Router.router(controller.getVertx());
router.route().handler(BodyHandler.create());
router.get("/").handler(this::handleIndex);
router.post("/benchmark").handler(this::handlePostBenchmark);
router.get("/benchmark").handler(this::handleListBenchmarks);
router.get("/benchmark/:benchmarkname").handler(this::handleGetBenchmark);
router.get("/benchmark/:benchmarkname/start").handler(this::handleBenchmarkStart);
router.get("/run").handler(this::handleListRuns);
router.get("/run/:runid").handler(this::handleGetRun);
router.get("/run/:runid/kill").handler(this::handleRunKill);
router.get("/run/:runid/sessions").handler(this::handleListSessions);
router.get("/run/:runid/sessions/recent").handler(this::handleRecentSessions);
router.get("/run/:runid/sessions/total").handler(this::handleTotalSessions);
router.get("/run/:runid/connections").handler(this::handleListConnections);
router.get("/run/:runid/stats/all").handler(this::handleAllStats);
router.get("/run/:runid/stats/recent").handler(this::handleRecentStats);
router.get("/run/:runid/stats/total").handler(this::handleTotalStats);
router.get("/run/:runid/stats/custom").handler(this::handleCustomStats);
router.get("/run/:runid/stats/histogram").handler(this::handleHistogramStats);
router.get("/run/:runid/benchmark").handler(this::handleRunBenchmark);
router.get("/agents").handler(this::handleAgents);
router.get("/log").handler(this::handleLog);
router.get("/log/:agent").handler(this::handleAgentLog);
router.get("/shutdown").handler(this::handleShutdown);
router.get("/version").handler(this::handleVersion);
httpServer = controller.getVertx().createHttpServer().requestHandler(router)
.listen(CONTROLLER_PORT, CONTROLLER_HOST, result -> {
if (result.succeeded()) {
HttpServer server = result.result();
baseURL = "http://" + CONTROLLER_HOST + ":" + server.actualPort();
}
handler.handle(result.mapEmpty());
});
}
void stop(Future stopFuture) {
httpServer.close(result -> stopFuture.complete());
}
private void handleIndex(RoutingContext ctx) {
StringBuilder sb = new StringBuilder("Hello from Hyperfoil, these are available URLs:\n");
for (Route route : router.getRoutes()) {
if (route.getPath() != null) { // avoid the default route
sb.append(route.getPath()).append('\n');
}
}
ctx.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
.putHeader("x-epoch-millis", String.valueOf(System.currentTimeMillis()))
.end(sb.toString());
}
private void handlePostBenchmark(RoutingContext ctx) {
String ctHeader = ctx.request().getHeader(HttpHeaders.CONTENT_TYPE);
String contentType = ctHeader == null ? "text/vnd.yaml" : ctHeader.trim();
Charset charset = StandardCharsets.UTF_8;
int semicolonIndex = contentType.indexOf(';');
if (semicolonIndex >= 0) {
String tmp = contentType.substring(semicolonIndex + 1).trim();
if (tmp.startsWith("charset=")) {
charset = Charset.forName(tmp.substring(8));
}
contentType = contentType.substring(0, semicolonIndex).trim();
}
Benchmark benchmark;
if (contentType.equals(MIME_TYPE_SERIALIZED)) {
byte[] bytes = ctx.getBody().getBytes();
try {
benchmark = io.hyperfoil.util.Util.deserialize(bytes);
} catch (IOException | ClassNotFoundException e) {
log.error("Failed to serialize", e);
ctx.response().setStatusCode(400).end("Cannot read benchmark.");
return;
}
} else if (MIME_TYPE_YAML.contains(contentType) || MIME_TYPE_JSON.equals(contentType)) {
String source = ctx.getBodyAsString(charset.name());
try {
benchmark = BenchmarkParser.instance().buildBenchmark(source, BenchmarkData.EMPTY);
} catch (ParserException | BenchmarkDefinitionException e) {
respondParsingError(ctx, e);
return;
}
} else if (MIME_TYPE_MULTIPART.equals(contentType)) {
String source = null;
RequestBenchmarkData data = new RequestBenchmarkData();
for (FileUpload upload : ctx.fileUploads()) {
byte[] bytes;
try {
bytes = Files.readAllBytes(Paths.get(upload.uploadedFileName()));
} catch (IOException e) {
log.error("Cannot read uploaded file {}", e, upload.uploadedFileName());
ctx.response().setStatusCode(500).end();
return;
}
if (upload.name().equals("benchmark")) {
try {
source = new String(bytes, upload.charSet());
} catch (UnsupportedEncodingException e) {
source = new String(bytes, StandardCharsets.UTF_8);
}
} else {
data.addFile(upload.fileName(), bytes);
}
}
if (source == null) {
ctx.response().setStatusCode(400).end("Multi-part definition missing benchmark=source-file.yaml");
return;
}
try {
benchmark = BenchmarkParser.instance().buildBenchmark(source, data);
} catch (ParserException | BenchmarkDefinitionException e) {
respondParsingError(ctx, e);
return;
}
} else {
ctx.response().setStatusCode(406).setStatusMessage("Unsupported Content-Type.");
return;
}
if (benchmark != null) {
String location = baseURL + "/benchmark/" + encode(benchmark.name());
controller.addBenchmark(benchmark, event -> {
if (event.succeeded()) {
ctx.response().setStatusCode(204)
.putHeader(HttpHeaders.LOCATION, location).end();
} else {
ctx.response().setStatusCode(500).end();
}
});
} else {
ctx.response().setStatusCode(400).end("Cannot read benchmark.");
}
}
private void respondParsingError(RoutingContext ctx, Exception e) {
log.error("Failed to read benchmark", e);
ctx.response().setStatusCode(400).end("Cannot read benchmark: " + Util.explainCauses(e));
}
private void handleListBenchmarks(RoutingContext routingContext) {
routingContext.response().setStatusCode(200).end(Json.encodePrettily(controller.getBenchmarks()));
}
private void handleGetBenchmark(RoutingContext ctx) {
String name = ctx.pathParam("benchmarkname");
Benchmark benchmark = controller.getBenchmark(name);
if (benchmark == null) {
ctx.response().setStatusCode(404).setStatusMessage("No benchmark '" + name + "'").end();
return;
}
sendBenchmark(ctx, benchmark);
}
private void handleRunBenchmark(RoutingContext ctx) {
Run run = getRun(ctx);
if (run == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
}
sendBenchmark(ctx, controller.ensureBenchmark(run));
}
private void sendBenchmark(RoutingContext ctx, Benchmark benchmark) {
String acceptHeader = ctx.request().getHeader(HttpHeaders.ACCEPT);
if (acceptHeader == null) {
ctx.response().setStatusCode(400).setStatusMessage("Missing Accept header in the request.").end();
return;
}
int semicolonIndex = acceptHeader.indexOf(';');
if (semicolonIndex >= 0) {
acceptHeader = acceptHeader.substring(0, semicolonIndex).trim();
}
if (acceptHeader.equals(MIME_TYPE_SERIALIZED)) {
try {
byte[] bytes = io.hyperfoil.util.Util.serialize(benchmark);
ctx.response().setStatusCode(200)
.putHeader(HttpHeaders.CONTENT_TYPE, MIME_TYPE_SERIALIZED)
.end(Buffer.buffer(bytes));
} catch (IOException e) {
log.error("Failed to serialize", e);
ctx.response().setStatusCode(500).end("Error encoding benchmark.");
}
} else if (MIME_TYPE_YAML.contains(acceptHeader) || "*/*".equals(acceptHeader)) {
if (benchmark.source() == null) {
ctx.response().setStatusCode(406).setStatusMessage("Benchmark does not preserve the original source.");
} else {
ctx.response().setStatusCode(200)
.putHeader(HttpHeaders.CONTENT_TYPE, "text/vnd.yaml; charset=UTF-8")
.end(benchmark.source());
}
} else {
ctx.response().setStatusCode(406).setStatusMessage("Unsupported type in Accept.").end();
}
}
private void handleBenchmarkStart(RoutingContext ctx) {
String benchmarkName = ctx.pathParam("benchmarkname");
Benchmark benchmark = controller.getBenchmark(benchmarkName);
String description = getSingleParam(ctx, "desc");
if (benchmark != null) {
if (TRIGGER_URL != null) {
String triggerJob = ctx.request().getHeader("x-trigger-job");
if (triggerJob == null) {
Run run = controller.createRun(benchmark, description);
ctx.response()
.setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code())
.putHeader(HttpHeaders.LOCATION, TRIGGER_URL + "BENCHMARK=" + benchmarkName + "&RUN_ID=" + run.id)
.putHeader("x-run-id", run.id)
.end("This controller is configured to trigger jobs through CI instance.");
return;
}
}
String runId = getSingleParam(ctx, "runId");
Run run;
if (runId == null) {
run = controller.createRun(benchmark, description);
} else {
run = controller.run(runId);
if (run == null || run.startTime != Long.MIN_VALUE) {
ctx.response().setStatusCode(HttpResponseStatus.FORBIDDEN.code()).end("Run already started");
return;
}
}
String error = controller.startBenchmark(run);
if (error == null) {
ctx.response().setStatusCode(HttpResponseStatus.ACCEPTED.code()).
putHeader(HttpHeaders.LOCATION, baseURL + "/run/" + run.id)
.end("Starting benchmark " + benchmarkName + ", run ID " + run.id);
} else {
ctx.response()
.setStatusCode(HttpResponseStatus.FORBIDDEN.code()).end(error);
}
} else {
ctx.response()
.setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end("Benchmark not found");
}
}
private String getSingleParam(RoutingContext ctx, String param) {
List list = ctx.queryParam(param);
String value = null;
if (list != null && !list.isEmpty()) {
value = list.iterator().next();
}
return value;
}
private void handleListRuns(RoutingContext ctx) {
String detailsStr = getSingleParam(ctx, "details");
boolean details = detailsStr != null && detailsStr.equalsIgnoreCase("true");
Client.Run[] runs = controller.runs().stream()
.map(r -> details ? runInfo(r, false) : new Client.Run(r.id, null, null, null, false, null, null, null, null))
.toArray(Client.Run[]::new);
ctx.response().setStatusCode(200).end(Json.encodePrettily(runs));
}
private void handleGetRun(RoutingContext routingContext) {
Run run = getRun(routingContext);
if (run == null) {
routingContext.response().setStatusCode(404).end();
return;
}
String status = Json.encodePrettily(runInfo(run, true));
routingContext.response().end(status);
}
private Client.Run runInfo(Run run, boolean reportPhases) {
String benchmark = null;
if (run.benchmark != null) {
benchmark = run.benchmark.name();
}
Date started = null, terminated = null;
if (run.startTime > Long.MIN_VALUE) {
started = new Date(run.startTime);
}
if (run.terminateTime.isComplete()) {
terminated = new Date(run.terminateTime.result());
}
List phases = null;
if (reportPhases) {
long now = System.currentTimeMillis();
phases = run.phases.values().stream()
.filter(p -> !(p.definition() instanceof Phase.Noop))
.sorted(PHASE_COMPARATOR)
.map(phase -> {
Date phaseStarted = null, phaseTerminated = null;
StringBuilder remaining = null;
StringBuilder totalDuration = null;
if (phase.absoluteStartTime() > Long.MIN_VALUE) {
phaseStarted = new Date(phase.absoluteStartTime());
if (!phase.status().isTerminated()) {
remaining = new StringBuilder()
.append(phase.definition().duration() - (now - phase.absoluteStartTime())).append(" ms");
if (phase.definition().maxDuration() >= 0) {
remaining.append(" (")
.append(phase.definition().maxDuration() - (now - phase.absoluteStartTime())).append(" ms)");
}
} else {
phaseTerminated = new Date(phase.absoluteCompletionTime());
long totalDurationValue = phase.absoluteCompletionTime() - phase.absoluteStartTime();
totalDuration = new StringBuilder().append(totalDurationValue).append(" ms");
if (totalDurationValue > phase.definition().duration()) {
totalDuration.append(" (exceeded by ").append(totalDurationValue - phase.definition().duration()).append(" ms)");
}
}
}
String type = phase.definition().getClass().getSimpleName();
type = Character.toLowerCase(type.charAt(0)) + type.substring(1);
return new Client.Phase(phase.definition().name(), phase.status().toString(), type,
phaseStarted, remaining == null ? null : remaining.toString(),
phaseTerminated, phase.isFailed(), totalDuration == null ? null : totalDuration.toString(),
phase.definition().description());
}).collect(Collectors.toList());
}
List agents = run.agents.stream()
.map(ai -> new Client.Agent(ai.name, ai.deploymentId, ai.status.toString()))
.collect(Collectors.toList());
return new Client.Run(run.id, benchmark, started, terminated, run.cancelled, run.description, phases, agents,
run.errors.stream().map(Run.Error::toString).collect(Collectors.toList()));
}
private Run getRun(RoutingContext routingContext) {
String runid = routingContext.pathParam("runid");
Run run;
if ("last".equals(runid)) {
run = controller.runs.values().stream()
.filter(r -> r.startTime > Long.MIN_VALUE)
.reduce((r1, r2) -> r1.startTime > r2.startTime ? r1 : r2)
.orElse(null);
} else {
run = controller.run(runid);
}
return run;
}
private void handleListSessions(RoutingContext routingContext) {
HttpServerResponse response = routingContext.response().setChunked(true);
boolean includeInactive = toBool(routingContext.queryParam("inactive"), false);
Run run = getRun(routingContext);
if (run == null) {
routingContext.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
}
controller.listSessions(run, includeInactive,
(agent, session) -> {
String line = agent.name + ": " + session + "\n";
response.write(Buffer.buffer(line.getBytes(StandardCharsets.UTF_8)));
},
commonListingHandler(response));
}
private boolean toBool(List params, boolean defaultValue) {
if (params.isEmpty()) {
return defaultValue;
}
return "true".equals(params.get(params.size() - 1));
}
private void handleListConnections(RoutingContext routingContext) {
HttpServerResponse response = routingContext.response().setChunked(true);
Run run = getRun(routingContext);
if (run == null) {
routingContext.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
}
controller.listConnections(run,
(agent, connection) -> {
String line = agent.name + ": " + connection + "\n";
response.write(Buffer.buffer(line.getBytes(StandardCharsets.UTF_8)));
},
commonListingHandler(response));
}
private Handler> commonListingHandler(HttpServerResponse response) {
return result -> {
if (result.succeeded()) {
response.setStatusCode(HttpResponseStatus.OK.code()).end();
} else if (result.cause() instanceof NoStackTraceThrowable) {
response.setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
} else {
response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end(result.cause().getMessage());
}
};
}
private void handleRunKill(RoutingContext routingContext) {
Run run = getRun(routingContext);
if (run != null) {
controller.kill(run, result -> {
if (result.succeeded()) {
routingContext.response().setStatusCode(202).end();
} else {
routingContext.response().setStatusCode(500).setStatusMessage(result.cause().getMessage()).end();
}
});
} else {
routingContext.response().setStatusCode(404).end();
}
}
private void handleAllStats(RoutingContext ctx) {
Run run = getRun(ctx);
if (run == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
} else if (!run.terminateTime.isComplete()) {
ctx.response().setStatusCode(HttpResponseStatus.SEE_OTHER.code())
.setStatusMessage("Run is not completed yet.")
.putHeader(HttpHeaders.LOCATION, "/run/" + run.id)
.end();
return;
}
String accept = ctx.request().getHeader(HttpHeaders.ACCEPT);
switch (accept) {
case MIME_TYPE_JSON:
ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, MIME_TYPE_JSON)
.sendFile(controller.getRunDir(run).resolve("all.json").toString());
break;
case MIME_TYPE_CSV:
new Zipper(ctx.response(), controller.getRunDir(run).resolve("stats")).run();
break;
default:
ctx.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage("Unknown format " + accept).end();
}
}
private void handleRecentStats(RoutingContext ctx) {
Run run = getRun(ctx);
if (run == null || run.statisticsStore == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
}
List stats = run.statisticsStore.recentSummary(System.currentTimeMillis() - 5000);
ctx.response().end(Json.encodePrettily(statsToJson(run, stats)));
}
private Client.RequestStatisticsResponse statsToJson(Run run, List stats) {
String status;
if (run.terminateTime.isComplete()) {
status = "TERMINATED";
} else if (run.startTime > Long.MIN_VALUE) {
status = "RUNNING";
} else {
status = "INITIALIZING";
}
return new Client.RequestStatisticsResponse(status, stats);
}
private void handleTotalStats(RoutingContext ctx) {
Run run = getRun(ctx);
if (run == null || run.statisticsStore == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
}
List stats = run.statisticsStore.totalSummary();
ctx.response().end(Json.encodePrettily(statsToJson(run, stats)));
}
private void handleCustomStats(RoutingContext ctx) {
Run run = getRun(ctx);
if (run == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
} else if (run.statisticsStore == null) {
ctx.response().end("{}");
return;
}
List stats = run.statisticsStore.customStats();
// TODO: add json response format based on 'Accept' header
ctx.response().end(new JsonArray(stats).encodePrettily());
}
private void handleHistogramStats(RoutingContext ctx) {
Run run = getRun(ctx);
if (run == null || run.statisticsStore == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
}
int stepId;
try {
stepId = Integer.parseInt(getSingleParam(ctx, "stepId"));
} catch (NumberFormatException e) {
ctx.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end();
return;
}
String phase = getSingleParam(ctx, "phase");
String metric = getSingleParam(ctx, "metric");
Client.Histogram histogram = run.statisticsStore.histogram(phase, stepId, metric);
ctx.response().end(Json.encode(histogram));
}
private void handleRecentSessions(RoutingContext ctx) {
handleSessionPoolStats(ctx, run -> run.statisticsStore.recentSessionPoolSummary(System.currentTimeMillis() - 5000));
}
private void handleTotalSessions(RoutingContext ctx) {
handleSessionPoolStats(ctx, run -> run.statisticsStore.totalSessionPoolSummary());
}
private void handleSessionPoolStats(RoutingContext ctx, Function>> func) {
Run run = getRun(ctx);
if (run == null) {
ctx.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
return;
} else if (run.statisticsStore == null) {
ctx.response().end("{}");
return;
}
Map> stats = func.apply(run);
JsonObject reply = new JsonObject();
stats.forEach((phase, addressStats) -> {
JsonObject phaseStats = new JsonObject();
reply.put(phase, phaseStats);
addressStats.forEach((address, lowHigh) -> {
String agent = run.agents.stream().filter(a -> a.deploymentId.equals(address)).map(a -> a.name).findFirst().orElse("unknown");
phaseStats.put(agent, new JsonObject().put("min", lowHigh.low).put("max", lowHigh.high));
});
});
ctx.response().end(reply.encodePrettily());
}
private void handleAgents(RoutingContext ctx) {
ctx.response().end(new JsonArray(controller.runs.values().stream()
.flatMap(run -> run.agents.stream().map(agentInfo -> agentInfo.name))
.distinct().collect(Collectors.toList())).encodePrettily());
}
private void handleLog(RoutingContext ctx) {
String logPath = System.getProperty(Properties.CONTROLLER_LOG);
if (logPath == null) {
ctx.response().setStatusCode(404).setStatusMessage("Log file not defined.").end();
return;
}
File logFile = new File(logPath);
if (!logFile.exists()) {
ctx.response().setStatusCode(404).setStatusMessage("Log file does not exist.").end();
} else {
long offset = getOffset(ctx);
if (offset >= 0) {
ctx.response().putHeader(HttpHeaders.ETAG, controller.deploymentID());
ctx.response().sendFile(logPath, offset);
}
}
}
private void handleAgentLog(RoutingContext ctx) {
String agent = ctx.pathParam("agent");
if (agent == null || "controller".equals(agent)) {
handleLog(ctx);
return;
}
long offset = getOffset(ctx);
if (offset < 0) {
return;
}
Optional agentInfo = controller.runs.values().stream()
.sorted(Comparator.comparing(run -> run.startTime).reversed())
.flatMap(run -> run.agents.stream())
.filter(ai -> agent.equals(ai.name)).findFirst();
if (!agentInfo.isPresent()) {
ctx.response().setStatusCode(404).setStatusMessage("Agent " + agent + " not found.").end();
return;
}
try {
File tempFile = File.createTempFile("agent." + agent, ".log");
tempFile.deleteOnExit();
controller.downloadAgentLog(agentInfo.get().deployedAgent, offset, tempFile, result -> {
if (result.succeeded()) {
ctx.response().putHeader(HttpHeaders.ETAG, agentInfo.get().deploymentId);
ctx.response().sendFile(tempFile.toString(), r -> tempFile.delete());
} else {
log.error("Failed to download agent log for " + agentInfo.get(), result.cause());
ctx.response().setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
.setStatusMessage("Cannot download agent log").end();
}
});
} catch (IOException e) {
log.error("Failed to create temporary file", e);
ctx.response().setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end();
}
}
private long getOffset(RoutingContext ctx) {
long offset = 0;
String offsetParam = ctx.request().getParam("offset");
if (offsetParam != null) {
try {
offset = Long.parseLong(offsetParam);
} catch (NumberFormatException e) {
ctx.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage("Malformed offset").end();
return -1;
}
}
if (offset < 0) {
ctx.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
.setStatusMessage("Offset must be non-negative").end();
return -1;
}
return offset;
}
private static String encode(String string) {
try {
return URLEncoder.encode(string, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
private void handleShutdown(RoutingContext ctx) {
boolean force = !ctx.queryParam("force").isEmpty();
List runs = controller.runs.values().stream().filter(run -> !run.terminateTime.isComplete()).collect(Collectors.toList());
if (force) {
// We don't allow concurrent runs ATM, but...
List futures = new ArrayList<>();
for (Run run : runs) {
Future future = Future.future();
futures.add(future);
controller.kill(run, result -> future.complete());
}
CompositeFuture.all(futures).setHandler(nil -> {
ctx.response().setStatusCode(200).end();
controller.shutdown();
});
} else if (runs.isEmpty()) {
ctx.response().setStatusCode(200).end();
controller.shutdown();
} else {
String running = runs.stream().map(run -> run.id).collect(Collectors.joining(", "));
ctx.response().setStatusCode(403)
.setStatusMessage("These runs are still in progress: " + running).end();
}
}
private void handleVersion(RoutingContext ctx) {
ctx.response().end(Json.encodePrettily(new Client.Version(Version.VERSION, Version.COMMIT_ID, controller.deploymentID())));
}
}