/* * 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.sling.discovery.commons.providers.spi.base; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base class which implements the concept of a 'BackgroundCheck', * a thread that periodically executes a check until that one succeeds. * <p> */ public abstract class AbstractServiceWithBackgroundCheck { protected final Logger logger = LoggerFactory.getLogger(getClass()); protected String slingId; /** * The BackgroundCheckRunnable implements the details of * calling BackgroundCheck.check and looping until it * returns true */ final class BackgroundCheckRunnable implements Runnable { private final Runnable callback; private final BackgroundCheck check; private final long timeoutMillis; private volatile boolean cancelled; private final String threadName; // for testing only: private final Object waitObj = new Object(); private int waitCnt; private volatile boolean done; private long waitInterval; private BackgroundCheckRunnable(Runnable callback, BackgroundCheck check, long timeoutMillis, long waitInterval, String threadName) { this.callback = callback; this.check = check; this.timeoutMillis = timeoutMillis; if (waitInterval <= 0) { throw new IllegalArgumentException("waitInterval must be greater than 0: "+waitInterval); } this.waitInterval = waitInterval; this.threadName = threadName; } boolean isDone() { synchronized(waitObj) { return done; } } @Override public void run() { logger.debug("backgroundCheck.run: start"); long start = System.currentTimeMillis(); try{ while(!cancelled()) { if (check.check()) { if (callback != null) { callback.run(); } return; } if (timeoutMillis != -1 && (System.currentTimeMillis() > start + timeoutMillis)) { if (callback == null) { logger.info("backgroundCheck.run: timeout hit (no callback to invoke)"); } else { logger.info("backgroundCheck.run: timeout hit, invoking callback."); callback.run(); } return; } logger.trace("backgroundCheck.run: waiting another sec."); synchronized(waitObj) { waitCnt++; try { waitObj.notify(); waitObj.wait(waitInterval); } catch (InterruptedException e) { logger.debug("backgroundCheck.run: got interrupted"); } } } logger.debug("backgroundCheck.run: this run got cancelled. {}", check); } catch(RuntimeException re) { logger.error("backgroundCheck.run: RuntimeException: "+re, re); // nevertheless calling runnable.run in this case if (callback != null) { logger.info("backgroundCheck.run: RuntimeException -> invoking callback"); callback.run(); } throw re; } catch(Error er) { logger.error("backgroundCheck.run: Error: "+er, er); // not calling runnable.run in this case! // since Error is typically severe logger.info("backgroundCheck.run: NOT invoking callback"); throw er; } finally { logger.debug("backgroundCheck.run: end"); synchronized(waitObj) { done = true; waitObj.notify(); } } } boolean cancelled() { return cancelled; } void cancel() { if (!done) { logger.info("cancel: "+threadName); } cancelled = true; } public void triggerCheck() { synchronized(waitObj) { int waitCntAtStart = waitCnt; waitObj.notify(); while(!done && waitCnt<=waitCntAtStart) { try { waitObj.wait(); } catch (InterruptedException e) { throw new RuntimeException("got interrupted"); } } } } } /** * A BackgroundCheck is anything that can be periodically * checked until it eventually returns true. */ interface BackgroundCheck { boolean check(); } protected BackgroundCheckRunnable backgroundCheckRunnable; /** * Cancel the currently ongoing background check if * there is any ongoing. */ protected void cancelPreviousBackgroundCheck() { BackgroundCheckRunnable current = backgroundCheckRunnable; if (current!=null) { current.cancel(); // leave backgroundCheckRunnable field as is // as that does not represent a memory leak // nor is it a problem to invoke cancel multiple times // but properly synchronizing on just setting backgroundCheckRunnable // back to null is error-prone and overkill } } /** * Start a new BackgroundCheck in a separate thread, that * periodically calls BackgroundCheck.check and upon completion * calls the provided callback.run() * @param threadName the name of the thread (to allow identifying the thread) * @param check the BackgroundCheck to periodically invoke with check() * @param callback the Runnable to invoke upon a successful check() * @param timeoutMillis a timeout at which point the BackgroundCheck is * terminated and no callback is invoked. Note that this happens unnoticed * at the moment, ie there is no feedback about whether a background * check was successfully termianted (ie callback was invoked) or * whether the timeout has hit (that's left as a TODO if needed). */ protected void startBackgroundCheck(String threadName, final BackgroundCheck check, final Runnable callback, final long timeoutMillis, final long waitMillis) { // cancel the current one if it's still running cancelPreviousBackgroundCheck(); if (check.check()) { // then we're not even going to start the background-thread // we're already done if (callback!=null) { logger.info("backgroundCheck: already done, backgroundCheck successful, invoking callback"); callback.run(); } else { logger.info("backgroundCheck: already done, backgroundCheck successful. no callback to invoke."); } return; } logger.info("backgroundCheck: spawning background-thread for '"+threadName+"'"); backgroundCheckRunnable = new BackgroundCheckRunnable(callback, check, timeoutMillis, waitMillis, threadName); Thread th = new Thread(backgroundCheckRunnable); th.setName(threadName); th.setDaemon(true); th.start(); } /** for testing only! **/ protected void triggerBackgroundCheck() { BackgroundCheckRunnable backgroundOp = backgroundCheckRunnable; if (backgroundOp!=null) { backgroundOp.triggerCheck(); } } }