/* * $Id$ * * Copyright 2011 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package omero.cmd; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import ome.conditions.InternalException; import ome.services.util.Executor; import ome.system.Principal; import ome.system.ServiceFactory; import ome.util.SqlAction; import omero.LockTimeout; import omero.ServerError; import org.hibernate.Session; import org.perf4j.StopWatch; import org.perf4j.slf4j.Slf4JStopWatch; import org.springframework.transaction.annotation.Transactional; import com.google.common.collect.MapMaker; import Ice.Current; import Ice.Identity; /** * Servant for the handle proxy from the Command API. This is also a * {@link Runnable} and is passed to a ThreadPool instance * * <pre> * Transitions: * * +------------------o [FINISHED] * | o * | | * (CREATED) ---o READY o===o RUNNING o===o CANCELLING ---o [CANCELLED] * | | o o * | |---------------------------| | * +--------------------------------------------------------+ * * </pre> * * @author Josh Moore, josh at glencoesoftware.com * @since 4.3.2 */ public class HandleI implements _HandleOperations, IHandle, SessionAware { private static enum State { CREATED, READY, RUNNING, CANCELLING, CANCELLED, FINISHED; } private static final long serialVersionUID = 15920349984928755L; private static final MapMaker mapMaker = new MapMaker(); /** * Timeout in seconds that cancellation should wait. */ private final int cancelTimeoutMs; /** * Callbacks that have been added by clients. */ private final Map<String, CmdCallbackPrx> callbacks = mapMaker.makeMap(); /** * State-diagram location. This instance is also used as a lock around * certain critical sections. */ private final AtomicReference<State> state = new AtomicReference<State>(); /** * Final response. If this value is non-null, then clients will assume that * processing is finished. */ private final AtomicReference<Response> rsp = new AtomicReference<Response>(); /** * Current step. should only be incremented during {@link #steps(SqlAction, Session, ServiceFactory)}. */ private final AtomicInteger currentStep = new AtomicInteger(); /** * Status which will be returned by {@link #getStatus()} and as a part of * the {@link Response} value. */ private final Status status = new Status(); /** * Context to be passed to * {@link Executor#execute(Map, Principal, ome.services.util.Executor.Work)} * for properly setting the call context. */ private/* final */Map<String, String> callContext; /** * The principal, i.e. the session information, about the current users * logins. This will be passed to the {@link #executor} instance for logging * in. */ private/* final */Principal principal; /** * Executor which will be used to provide access to the Hibernate * {@link Session} in a background thread. */ private/* final */Executor executor; /** * The identity of this servant, used during logging and similar operations. */ private/* final */Ice.Identity id; private/* final */SessionI sess; private/* final */IRequest req; private/* final */Helper helper; // // INTIALIZATION // /** * Create a {@link HandleI} (at {@code CREATED} in the state diagram) * with the given cancel timeout in milliseconds. * @param cancelTimeoutMs the cancel timeout (in milliseconds) */ public HandleI(int cancelTimeoutMs) { this.cancelTimeoutMs = cancelTimeoutMs; this.state.set(State.CREATED); } public void setSession(SessionI session) throws ServerError { this.sess = session; this.principal = sess.getPrincipal(); this.executor = sess.getExecutor(); } public void initialize(Identity id, IRequest req, Map<String, String> ctx) { this.id = id; this.req = req; this.callContext = ctx; this.helper = new Helper((Request)req, status, null, null, null); } // // CALLBACKS // public void addCallback(CmdCallbackPrx cb, Current __current) { Ice.Identity id = cb.ice_getIdentity(); String key = Ice.Util.identityToString(id); helper.info("Add callback: %s", key); cb = CmdCallbackPrxHelper.checkedCast(cb.ice_oneway()); callbacks.put(key, cb); } public void removeCallback(CmdCallbackPrx cb, Current __current) { Ice.Identity id = cb.ice_getIdentity(); String key = Ice.Util.identityToString(id); helper.info("Remove callback: %s", key); cb = CmdCallbackPrxHelper.checkedCast(cb.ice_oneway()); callbacks.remove(key); } /** * Calls the proper notification on all callbacks based on the current * position in the state diagram. If that is anything other than * {@code CANCELLED} or {@code FINISHED} then * {@link CmdCallbackPrx#step(int, int)} is called. */ public void notifyCallbacks() { final State state = this.state.get(); final boolean finished = state.equals(State.FINISHED); final boolean cancelled = state.equals(State.CANCELLED); for (final CmdCallbackPrx prx : callbacks.values()) { try { Response rsp = this.rsp.get(); if (finished || cancelled) { if (cancelled) { helper.info("notify cancelled: %s/%s", rsp, status); } else { helper.info("notify finished: %s/%s", rsp, status); } prx.finished(rsp, status); } else { int step = currentStep.get(); helper.info("notify step %s of %s", step, status.steps); prx.step(step, status.steps); } } catch (Exception e) { sess.handleCallbackException(e); } } } // // GETTERS // public Request getRequest(Current __current) { helper.info("getRequest: %s", req); return (Request) req; } public Response getResponse(Current __current) { Response rsp = this.rsp.get(); helper.info("getResponse: %s", rsp); return rsp; } public Status getStatus(Current __current) { helper.info("getStatus: %s", status); return status; } // // STATE MGMT // public boolean cancel(Current __current) throws LockTimeout { try { boolean cancelled = cancelWithoutNotification(); if (cancelled) { try { helper.cancel(new ERR(), null, "cancel-called"); } catch (Cancel c) { // Duh. we're cancel'ing. This is expected. } } return cancelled; } finally { notifyCallbacks(); } } private boolean cancelWithoutNotification() throws LockTimeout { helper.info("Cancelling..."); // If we can successfully catch the CREATED or READY state, // then there's no reason to wait for anything else. if (state.compareAndSet(State.CREATED, State.CANCELLED) || state.compareAndSet(State.READY, State.CANCELLED)) { return true; } long start = System.currentTimeMillis(); while (cancelTimeoutMs >= (System.currentTimeMillis() - start)) { // This is the most important case. If things are running, then // we want to set "CANCELLING" as quickly as possible. if (state.compareAndSet(State.RUNNING, State.CANCELLING) || state.compareAndSet(State.READY, State.CANCELLING) || state.compareAndSet(State.CANCELLING, State.CANCELLING)) { try { Thread.sleep(cancelTimeoutMs / 10); } catch (InterruptedException e) { // Igoring the interruption since the while block // will properly handle another iteration. } } // These are end states, so we'll just return. // See the transition states in the class javadoc if (state.compareAndSet(State.CANCELLED, State.CANCELLED)) { return true; } else if (state.compareAndSet(State.FINISHED, State.FINISHED)) { return false; } } // The only time that state gets set to CANCELLING is in the try // block above. If we've exited the while without switching to CANCELLED // then we've failed, and the value should be rolled back. // // If #run() noticed this before hand, then it would already have // moved from RUNNING to CANCELLED, and so the following statement would // return false, in which case we print out a warning so that in a later // version we can be more careful about retrying. if (!state.compareAndSet(State.CANCELLING, State.RUNNING)) { helper.warn("Can't reset to RUNNING. State already changed.\n" + "This could be caused either by another thread having\n" + "already set the state back to RUNNING, or by the state\n" + "having changed to CANCELLED. In either case, it is safe\n" + "to throw the exception, and have the user recall cancel."); } LockTimeout lt = new LockTimeout(); lt.backOff = 5000; lt.message = "timed out while waiting on CANCELLED state"; lt.seconds = cancelTimeoutMs / 1000; throw lt; } // // CloseableServant. See documentation in interface. // public void close(Ice.Current current) { sess.unregisterServant(id); // No exception try { closeWithoutNotification(current); } finally { notifyCallbacks(); } } private void closeWithoutNotification(Ice.Current current) { helper.info("Closing..."); final State s = state.get(); if (!State.FINISHED.equals(s) && !State.CANCELLED.equals(s)) { helper.info("Handle closed before finished! State=" + state.get()); try { cancel(current); } catch (LockTimeout e) { helper.warn("Cancel failed"); } } } // // Runnable // /** * * NB: Executes only if at {@code CREATED} in the state diagram. */ public void run() { // If we're not in the created state, then do nothing // since something has gone wrong. if (!state.compareAndSet(State.CREATED, State.READY)) { return; // EARLY EXIT! } StopWatch sw = new Slf4JStopWatch(); try { Map<String, String> merged = mergeContexts(); @SuppressWarnings("unchecked") List<Object> rv = (List<Object>) executor.execute(merged, principal, new Executor.SimpleWork(this, "run", Ice.Util.identityToString(id), req) { @Transactional(readOnly = false) public List<Object> doWork(Session session, ServiceFactory sf) { try { List<Object> rv = steps(getSqlAction(), session, sf); state.set(State.FINISHED); // Regardless of current return rv; } catch (Cancel c) { // TODO: Perhaps remove local State enum and use solely // the slice defined one. state.set(State.CANCELLED); throw c; // Exception intended to rollback transaction } } }); // Post-process for (int step = 0; step < status.steps; step++) { Object obj = rv.get(step); req.buildResponse(step, obj); } } catch (Cancel cancel) { helper.debug("Request cancelled by %s", cancel.getCause()); // If this is a cancel, then fail or similar has already // been called and the response will be properly set. } catch (Throwable t) { helper.warn("Request rolled back by %s", t.getCause()); helper.fail(new ERR(), t, "run-fail"); } finally { // getResponse will be called regardless of return/exception state // and therefore any cleanup can happen there as soon as the response // is non-null. rsp.set(req.getResponse()); sw.stop("omero.request.tx"); notifyCallbacks(); } } private Map<String, String> mergeContexts() { final Map<String, String> merged = new HashMap<String, String>(); final Map<String, String> reqCctx = req.getCallContext(); if (callContext != null) { helper.debug("User callContext: %s", callContext); merged.putAll(callContext); } if (reqCctx != null) { helper.debug("Request callContext: %s", reqCctx); merged.putAll(reqCctx); } return merged; } public List<Object> steps(SqlAction sql, Session session, ServiceFactory sf) throws Cancel { StopWatch swWhole = new Slf4JStopWatch(); try { // Initialize. Any exceptions should cancel the process List<Object> rv = new ArrayList<Object>(); StopWatch swEach = null; // Now that we're in the transaction, replace the helper. helper = new Helper((Request)req, status, sql, session, sf); req.init(helper); int j = 0; while (j < status.steps) { swEach = new Slf4JStopWatch(); try { if (!state.compareAndSet(State.READY, State.RUNNING)) { throw helper.cancel(new ERR(), null, "not-ready"); } status.currentStep = j; rv.add(req.step(j)); } catch (Cancel c) { throw c; } catch (Throwable t) { throw helper.cancel(new ERR(), t, "bad-step", "step", ""+j); } finally { swEach.stop("omero.request.step." + j); // If cancel was thrown, then this value will be overwritten // by the try/catch handler state.compareAndSet(State.RUNNING, State.READY); } j = currentStep.incrementAndGet(); // SOLE INCREMENT // The following would probably be better handled by a // background thread, or via the heartbeat mechanism. For // the moment, though we'll notify callbacks per decile. int numOfCallbacks = 10; // TODO: configurable int mod = 1; // status.steps == 0 can't happen if (status.steps > numOfCallbacks) { mod = (status.steps / numOfCallbacks); } if ((j % mod) == 0) { notifyCallbacks(); } } req.finish(); return rv; } catch (Cancel cancel) { throw cancel; } catch (Throwable t) { String msg = "Failure during Request.step:"; helper.error(t, msg); throw helper.cancel(new ERR(), t, "steps-cancel"); } finally { swWhole.stop("omero.request"); status.startTime = swWhole.getStartTime(); status.stopTime = swWhole.getStartTime() + swWhole.getElapsedTime(); } } /** * Signals that {@link HandleI#run()} has noticed that the state diagram * wants a cancellation or that the {@link Request} implementation wishes to * stop execution. */ public static class Cancel extends InternalException { private static final long serialVersionUID = 1L; public Cancel(String message) { super(message); } } }