RestartCounter.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.pipes.core;
import java.util.EnumMap;
import java.util.concurrent.atomic.LongAdder;
import org.apache.tika.pipes.core.protocol.PipesMessageType;
import org.apache.tika.pipes.core.server.PipesServer;
/**
* Reason marked while a restart is pending, plus counts of restarts performed, by reason.
* Last mark wins; an unmarked restart is attributed by the old process's exit code.
*/
final class RestartCounter {
private final EnumMap<RestartReason, LongAdder> counts = new EnumMap<>(RestartReason.class);
private volatile RestartReason pending;
RestartCounter() {
for (RestartReason r : RestartReason.values()) {
counts.put(r, new LongAdder());
}
}
void mark(RestartReason reason) {
pending = reason;
}
/** Marks {@code reason} unless a more specific one is already pending. */
void markIfUnmarked(RestartReason reason) {
if (pending == null) {
pending = reason;
}
}
/**
* Records a restart of {@code previous}. Null means there was no process to restart
* (first start, or the last start failed): nothing is counted and any mark is dropped,
* since it referred to a restart that never happened.
*/
void restarted(Process previous) {
if (previous == null) {
pending = null;
return;
}
restarted(previous.isAlive() ? -1 : previous.exitValue());
}
/** Unmarked restarts are attributed by the exit code the child chose. */
void restarted(int exitCode) {
RestartReason reason = pending;
pending = null;
if (reason == null) {
reason = fromExitCode(exitCode);
}
counts.get(reason).increment();
}
/**
* The child reports why it died in its exit status, and the parent may reach a restart
* without ever having read the corresponding frame (the socket can break first, or the
* child can outlive the parent's one-second wait). Every code the child can deliberately
* choose is honoured here; only genuinely unexplained deaths fall through to CRASH.
*/
private static RestartReason fromExitCode(int exitCode) {
if (exitCode == PipesServer.IDLE_EXIT_CODE) {
return RestartReason.IDLE;
}
if (exitCode == 0) {
// Only PipesServer's SHUT_DOWN handler exits 0, and the parent is the only sender:
// a worker we asked to stop is not a crash, however we noticed it was gone.
return RestartReason.SHUTDOWN;
}
if (matches(PipesMessageType.OOM, exitCode)) {
return RestartReason.OOM;
}
if (matches(PipesMessageType.TIMEOUT, exitCode)) {
return RestartReason.TIMEOUT;
}
return RestartReason.CRASH;
}
private static boolean matches(PipesMessageType type, int exitCode) {
return type.getExitCode().orElse(Integer.MIN_VALUE) == exitCode;
}
long count(RestartReason reason) {
return counts.get(reason).sum();
}
}