/*
* $Id$
*
* Copyright 2011 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.pixeldata;
import java.sql.Timestamp;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import ome.conditions.InternalException;
import ome.io.messages.MissingPyramidMessage;
import ome.model.core.Pixels;
import ome.model.enums.EventType;
import ome.model.meta.Event;
import ome.model.meta.EventLog;
import ome.model.meta.Experimenter;
import ome.model.meta.ExperimenterGroup;
import ome.security.basic.CurrentDetails;
import ome.services.sessions.SessionManager;
import ome.services.util.ExecutionThread;
import ome.services.util.Executor;
import ome.system.EventContext;
import ome.system.Principal;
import ome.system.ServiceFactory;
import ome.system.metrics.Metrics;
import ome.system.metrics.NullMetrics;
import ome.system.metrics.Timer;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.transaction.annotation.Transactional;
/**
*
* @author Josh Moore, josh at glencoesoftware.com
* @since Beta4.3
*/
public class PixelDataThread extends ExecutionThread implements ApplicationListener<MissingPyramidMessage> {
private final static Logger log = LoggerFactory.getLogger(PixelDataThread.class);
private final static Principal DEFAULT_PRINCIPAL = new Principal("root",
"system", "Task");
private final static int DEFAULT_THREADS = 1;
/** Server session UUID */
private final String uuid;
/** Number of threads that should be used for processing **/
private final int numThreads;
/**
* Whether this thread should perform actual processing or simply add a
* PIXELDATA {@link EventLog}. For the moment, this is determined based
* on whether "pixelDataTrigger" is defined. If yes, then this is the
* standalone pixel data processor using the ome/services/pixeldata.xml
* Spring configuration. Otherwise, it's the main blitz process.
*/
private final boolean performProcessing;
private final Timer batchTimer;
/**
* Uses default {@link Principal} for processing
*/
public PixelDataThread(SessionManager manager, Executor executor,
PixelDataHandler handler, String uuid) {
this(manager, executor, handler, DEFAULT_PRINCIPAL, uuid, DEFAULT_THREADS);
}
/**
* Uses default {@link Principal} for processing and a {@link NullMetrics}
* instance.
*/
public PixelDataThread(SessionManager manager, Executor executor,
PixelDataHandler handler, String uuid, int numThreads) {
this(manager, executor, handler, DEFAULT_PRINCIPAL, uuid, numThreads,
new NullMetrics());
}
/**
* Calculates {@link #performProcessing} based on the existence of the
* "pixelDataTrigger" and passes all parameters to
* {@link #PixelDataThread(boolean, SessionManager, Executor, PixelDataHandler, Principal, String, int) the main ctor}
* passing a {@link NullMetrics} as necessary.
*/
public PixelDataThread(SessionManager manager, Executor executor,
PixelDataHandler handler, Principal principal, String uuid,
int numThreads) {
this(executor.getContext().containsBean("pixelDataTrigger"),
manager, executor, handler, principal, uuid, numThreads,
new NullMetrics());
}
/**
* Calculates {@link #performProcessing} based on the existence of the
* "pixelDataTrigger" and passes all parameters to
* {@link #PixelDataThread(boolean, SessionManager, Executor, PixelDataHandler, Principal, String, int) the main ctor}.
*/
public PixelDataThread(
SessionManager manager, Executor executor,
PixelDataHandler handler, String uuid,
int numThreads, Metrics metrics) {
this(executor.getContext().containsBean("pixelDataTrigger"),
manager, executor, handler, DEFAULT_PRINCIPAL,
uuid, numThreads, metrics);
}
/**
* Calculates {@link #performProcessing} based on the existence of the
* "pixelDataTrigger" and passes all parameters to
* {@link #PixelDataThread(boolean, SessionManager, Executor, PixelDataHandler, Principal, String, int) the main ctor}.
*/
public PixelDataThread(
SessionManager manager, Executor executor,
PixelDataHandler handler, Principal principal, String uuid,
int numThreads, Metrics metrics) {
this(executor.getContext().containsBean("pixelDataTrigger"),
manager, executor, handler, principal,
uuid, numThreads, metrics);
}
/**
* Calls main constructor with {@link NullMetrics}.
*/
public PixelDataThread(boolean performProcessing,
SessionManager manager, Executor executor,
PixelDataHandler handler, Principal principal, String uuid,
int numThreads) {
this(performProcessing, manager, executor, handler, principal,
uuid, numThreads, new NullMetrics());
}
/**
* Main constructor. No arguments can be null.
*/
public PixelDataThread(boolean performProcessing,
SessionManager manager, Executor executor,
PixelDataHandler handler, Principal principal, String uuid,
int numThreads, Metrics metrics) {
super(manager, executor, handler, principal);
this.performProcessing = performProcessing;
this.uuid = uuid;
this.numThreads = numThreads;
this.batchTimer = metrics.timer(this, "batch");
}
/**
* Called by Spring on creation. Currently a no-op.
*/
public void start() {
StringBuilder sb = new StringBuilder();
sb.append("Initializing PixelDataThread");
if (performProcessing) {
sb.append(String.format(" (threads=%s)", numThreads));
} else {
sb.append(" (create events only)");
}
log.info(sb.toString());
}
/**
* Loads event logs from the {@link PixelDataHandler} processing them
* all then in a background thread via a {@link ExecutorCompletionService}.
*
* {@link #numThreads} variable is also used there, so the value returned
* <em>should</em> match. In case it isn't, we additionally use an
* {@link ArrayBlockingQueue} to hold the results.
*/
@Override
public void doRun() {
if (performProcessing) {
final ExecutorCompletionService<Object> ecs =
new ExecutorCompletionService<Object>(executor.getService(),
new ArrayBlockingQueue<Future<Object>>(numThreads));
@SuppressWarnings("unchecked")
List<EventLog> eventLogs = (List<EventLog>)
executor.execute(getPrincipal(), work);
for (final EventLog log : eventLogs) {
ecs.submit(new Callable<Object>(){
@Override
public Object call()
throws Exception
{
return go(log);
}
});
}
int count = eventLogs.size();
while (count > 0) {
try {
Future<Object> future = ecs.poll(500, TimeUnit.MILLISECONDS);
if (future != null && future.get() != null) {
count--;
}
} catch (ExecutionException ee) {
onExecutionException(ee);
} catch (InterruptedException ie) {
log.debug("Interrupted; looping", ie);
}
}
}
}
/**
* {@link Executor.Work} implementation for the second phase of PixelData
* processing. Once the {@link EventLog} instances are available, each
* should be passed to a new {@link HandleEventLog} instance and then
* processed in a background thread.
*/
private static class HandleEventLog extends Executor.SimpleWork {
private final PixelDataHandler handler;
private final EventLog log;
HandleEventLog(EventLog log, PixelDataHandler handler,
Object self, String description, Object...args) {
super(self, description, args);
this.handler = handler;
this.log = log;
}
@Transactional(readOnly=false)
@Override
public Object doWork(Session session, ServiceFactory sf) {
this.handler.handleEventLog(log, session, sf);
return null;
}
}
private Object go(EventLog log) {
final Timer.Context timer = batchTimer.time();
try {
executor.execute(getPrincipal(),
new HandleEventLog(log, (PixelDataHandler) work,
this, "handleEventLog"));
return log;
} finally {
timer.stop();
}
}
/**
* Basic handling just logs at ERROR level. Subclasses (especially for
* testing) can do more.
*/
protected void onExecutionException(ExecutionException ee) {
log.error("ExceptionException!", ee.getCause());
}
/**
* Called by Spring on destruction.
*/
public void stop() {
log.info("Shutting down PixelDataThread");
((PixelDataHandler) this.work).loader.setStop(true);
}
/**
* Called in the main server (Blitz-0) in order to create a PIXELDATA
* {@link EventLog} which will get processed by PixelData-0.
*/
public void onApplicationEvent(final MissingPyramidMessage mpm) {
log.info("Received: " + mpm);
// #5232. If this is called without an active event, then throw
// an exception since a call to Executor should wrap whatever the
// invoker is doing.
final CurrentDetails cd = executor.getContext().getBean(CurrentDetails.class);
if (cd.size() <= 0) {
throw new InternalException("Not logged in.");
}
final EventContext ec = cd.getCurrentEventContext();
if (null == ec.getCurrentUserId()) {
throw new InternalException("No user! Must be wrapped by call to Executor?");
}
Future<EventLog> future = this.executor.submit(cd.getContext(),
new Callable<EventLog>(){
public EventLog call() throws Exception {
return makeEvent(ec, mpm);
}});
this.executor.get(future);
}
private EventLog makeEvent(final EventContext ec,
final MissingPyramidMessage mpm) {
final Principal p = new Principal(uuid);
final Map<String, String> callContext = new HashMap<String, String>();
// First call is with -1 in order to find the pixels group.
// TODO: this could equally be done with sqlAction.
callContext.put("omero.group", "-1");
final Long groupID = (Long) this.executor.execute(callContext, p,
new Executor.SimpleWork(this, "getGroupId") {
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
final ExperimenterGroup group = sf.getQueryService().findByQuery(
"select p.details.group from Pixels p where p.id = :id",
new ome.parameters.Parameters().addId(mpm.pixelsID));
return group.getId();
}
});
// Reset to prevent "Not intended for copying" errors
callContext.put("omero.group", groupID.toString());
return (EventLog) this.executor.execute(callContext, p,
new Executor.SimpleWork(this, "createEvent") {
@Transactional(readOnly = false)
public Object doWork(Session session, ServiceFactory sf) {
log.info("Creating PIXELDATA event for pixels id:"
+ mpm.pixelsID);
// Load objects
final EventType type = sf.getTypesService().getEnumeration(
EventType.class, ec.getCurrentEventType());
final EventLog el = new EventLog();
final Event e = new Event();
e.setExperimenter(
new Experimenter(ec.getCurrentUserId(), false));
e.setExperimenterGroup(new ExperimenterGroup(groupID, false));
e.setSession(new ome.model.meta.Session(
ec.getCurrentSessionId(), false));
e.setTime(new Timestamp(new Date().getTime()));
e.setType(type);
el.setAction("PIXELDATA");
el.setEntityId(mpm.pixelsID);
el.setEntityType(Pixels.class.getName());
el.setEvent(e);
return sf.getUpdateService().saveAndReturnObject(el);
}
});
}
}