TikaTimeoutException.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.exception;

/**
 * Thrown when a single operation (external process, HTTP call, pool borrow, etc.)
 * exceeds its allotted timeout.
 * <p>
 * Checked, not a {@link RuntimeException}: expected to be caught at the nearest
 * embedded-document boundary (see {@code ParsingEmbeddedDocumentExtractor}), recorded,
 * and parsing of remaining siblings continued. This differs from the task's total
 * deadline being exhausted, which does not throw at all -- see {@code ParseRecord}'s
 * {@code taskDeadlineReached} handling.
 * <p>
 * When the caller got its budget via {@code ParseTimeout.budgetFor(long)}, use
 * {@link #TikaTimeoutException(String, long, long)} so the message states both the
 * requested and granted budget: {@code granted == requested} means the operation's own
 * timeout was binding; {@code granted < requested} means the task's total timeout was
 * binding and raising the operation's timeout won't help.
 *
 * @since Apache Tika 4.0
 */
public class TikaTimeoutException extends TikaException {

    private static final long UNKNOWN = -1;

    private final long requestedMillis;
    private final long grantedMillis;

    public TikaTimeoutException(String message) {
        this(message, UNKNOWN, UNKNOWN);
    }

    /**
     * @param requestedMillis the timeout the caller's own configuration asked for
     * @param grantedMillis   the budget actually granted, e.g. by {@code ParseTimeout.budgetFor}
     */
    public TikaTimeoutException(String message, long requestedMillis, long grantedMillis) {
        super(buildMessage(message, requestedMillis, grantedMillis));
        this.requestedMillis = requestedMillis;
        this.grantedMillis = grantedMillis;
    }

    private static String buildMessage(String message, long requestedMillis, long grantedMillis) {
        if (requestedMillis == UNKNOWN || grantedMillis == UNKNOWN) {
            return message;
        }
        String clippedBy = grantedMillis < requestedMillis ? " (task remaining)" : " -- budget exhausted";
        return message + ": requested=" + requestedMillis + "ms, granted=" + grantedMillis + "ms" + clippedBy;
    }

    /**
     * @return the timeout the caller's own configuration requested, or {@code -1} if
     * this exception was not constructed with that information
     */
    public long getRequestedMillis() {
        return requestedMillis;
    }

    /**
     * @return the budget actually granted, or {@code -1} if this exception was not
     * constructed with that information
     */
    public long getGrantedMillis() {
        return grantedMillis;
    }

    /**
     * @return true if the granted budget was clipped below the requested timeout by the
     * task's remaining time (the task's total timeout was binding, not the operation's
     * own). False -- including when no requested/granted info was recorded -- means the
     * operation's own timeout was binding.
     */
    public boolean isClippedByRemaining() {
        return requestedMillis != UNKNOWN && grantedMillis != UNKNOWN && grantedMillis < requestedMillis;
    }
}