aboutsummaryrefslogtreecommitdiffstats
path: root/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/ChildProcess2Impl.java
blob: 8574028b6d7fa4bf8d1acebd710b0c5b5a5e2889 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.

package com.yahoo.vespa.hosted.node.admin.task.util.process;

import com.yahoo.jdisc.Timer;
import java.util.logging.Level;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import static com.yahoo.yolean.Exceptions.uncheck;

/**
 * @author hakonhall
 */
public class ChildProcess2Impl implements ChildProcess2 {
    private static final Logger logger = Logger.getLogger(ChildProcess2Impl.class.getName());

    private final CommandLine commandLine;
    private final ProcessApi2 process;
    private final Path outputPath;
    private final Timer timer;

    public ChildProcess2Impl(CommandLine commandLine,
                             ProcessApi2 process,
                             Path outputPath,
                             Timer timer) {
        this.commandLine = commandLine;
        this.process = process;
        this.outputPath = outputPath;
        this.timer = timer;
    }

    @Override
    public void waitForTermination() {
        Duration timeoutDuration = commandLine.getTimeout();
        Instant timeout = timer.currentTime().plus(timeoutDuration);
        long maxOutputBytes = commandLine.getMaxOutputBytes();

        // How frequently do we want to wake up and check the output file size?
        final Duration pollInterval = Duration.ofSeconds(10);

        boolean hasTerminated = false;
        while (!hasTerminated) {
            Instant now = timer.currentTime();
            long sleepPeriodMillis = pollInterval.toMillis();
            if (now.plusMillis(sleepPeriodMillis).isAfter(timeout)) {
                sleepPeriodMillis = Duration.between(now, timeout).toMillis();

                if (sleepPeriodMillis <= 0) {
                    gracefullyKill();
                    throw new TimeoutChildProcessException(
                            timeoutDuration, commandLine.toString(), getOutput());
                }
            }

            try {
                hasTerminated = process.waitFor(sleepPeriodMillis, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                // Ignore, just loop around.
                continue;
            }

            // Always check output file size to ensure we don't load too much into memory.
            long sizeInBytes = uncheck(() -> Files.size(outputPath));
            if (sizeInBytes > maxOutputBytes) {
                gracefullyKill();
                throw new LargeOutputChildProcessException(
                        sizeInBytes, commandLine.toString(), getOutput());
            }
        }
    }

    @Override
    public int exitCode() {
        return process.exitValue();
    }

    @Override
    public String getOutput() {
        byte[] bytes = uncheck(() -> Files.readAllBytes(outputPath));
        return new String(bytes, commandLine.getOutputEncoding());
    }

    @Override
    public void close() {
        try {
            if (commandLine.getOutputFile().isEmpty())
                Files.delete(outputPath);
        } catch (Throwable t) {
            logger.log(Level.WARNING, "Failed to delete " + outputPath, t);
        }
    }

    Path getOutputPath() {
        return outputPath;
    }

    private void gracefullyKill() {
        process.destroy();

        Duration maxWaitAfterSigTerm = commandLine.getSigTermGracePeriod();
        Instant timeout = timer.currentTime().plus(maxWaitAfterSigTerm);
        if (!waitForTermination(timeout)) {
            process.destroyForcibly();

            // If waiting for the process now takes a long time, it's probably a kernel issue
            // or huge core is getting dumped.
            Duration maxWaitAfterSigKill = commandLine.getSigKillGracePeriod();
            if (!waitForTermination(timer.currentTime().plus(maxWaitAfterSigKill))) {
                throw new UnkillableChildProcessException(
                        maxWaitAfterSigTerm,
                        maxWaitAfterSigKill,
                        commandLine.toString(),
                        getOutput());
            }
        }
    }

    /** @return true if process terminated, false on timeout. */
    private boolean waitForTermination(Instant timeout) {
        while (true) {
            long waitDurationMillis = Duration.between(timer.currentTime(), timeout).toMillis();
            if (waitDurationMillis <= 0) {
                return false;
            }

            try {
                return process.waitFor(waitDurationMillis, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }
}