// Copyright 2017 JanusGraph Authors // // Licensed 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.janusgraph.util.system; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; /** * @author Matthias Broecheler (me@matthiasb.com) */ public abstract class BackgroundThread extends Thread { private static final Logger log = LoggerFactory.getLogger(BackgroundThread.class); private volatile boolean interruptible = true; private volatile boolean softInterrupted = false; /** * * NEVER set daemon=true and override the cleanup() method. If this is a daemon thread there is no guarantee that * cleanup is called. * * @param name * @param daemon */ public BackgroundThread(String name, boolean daemon) { this.setName(name + ":" + getId()); this.setDaemon(daemon); } @Override public void run() { /* We use interrupted() instead of isInterrupted() to guarantee that the * interrupt flag is cleared when we exit this loop. cleanup() can then * run blocking operations without failing due to interruption. */ while (!interrupted() && !softInterrupted) { try { waitCondition(); } catch (InterruptedException e) { log.debug("Interrupted in background thread wait condition", e); break; } /* This check could be removed without affecting correctness. At * worst, removing it should just reduce shutdown responsiveness in * a couple of corner cases: * * 1. Rare interruptions are those that occur while this thread is * in the RUNNABLE state * * 2. Odd waitCondition() implementations that swallow an * InterruptedException and set the interrupt status instead of just * propagating the InterruptedException to us */ if (interrupted()) break; interruptible = false; try { action(); } catch (Throwable e) { log.error("Exception while executing action on background thread",e); } finally { /* * This doesn't really need to be in a finally block as long as * we catch Throwable, but it's here as future-proofing in case * the catch-clause type is narrowed in future revisions. */ interruptible = true; } } try { cleanup(); } catch (Throwable e) { log.error("Exception while executing cleanup on background thread",e); } } /** * The wait condition for the background thread. This determines what this background thread is waiting for in * its execution. This might be elapsing time or availability of resources. * * Since there is a wait involved, this method should throw an InterruptedException * * @throws InterruptedException */ protected abstract void waitCondition() throws InterruptedException; /** * The action taken by this background thread when the wait condition is met. * This action should execute swiftly to ensure that this thread can be closed in a reasonable amount of time. * * This action will not be interrupted by {@link #close(Duration)}. */ protected abstract void action(); /** * Any clean up that needs to be done before this thread is closed down. */ protected void cleanup() { //Do nothing by default } public void close(Duration duration) { if (!isAlive()) { log.warn("Already closed: {}", this); return; } final long maxWaitMs = duration.toMillis(); softInterrupted = true; if (interruptible) interrupt(); try { join(maxWaitMs); } catch (InterruptedException e) { log.error("Interrupted while waiting for thread {} to join",e); } if (isAlive()) { log.error("Thread {} did not terminate in time [{}]. This could mean that important clean up functions could not be called.", getName(), maxWaitMs); } } }