package org.marketcetera.marketdata.core.webservice.impl; import java.util.Collection; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import org.apache.commons.lang.Validate; import org.marketcetera.core.CloseableLock; import org.marketcetera.core.publisher.ISubscriber; import org.marketcetera.event.AggregateEvent; import org.marketcetera.event.Event; import org.marketcetera.marketdata.Capability; import org.marketcetera.marketdata.Content; import org.marketcetera.marketdata.MarketDataRequest; import org.marketcetera.marketdata.core.manager.MarketDataManager; import org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter; import org.marketcetera.marketdata.core.webservice.ConnectionException; import org.marketcetera.marketdata.core.webservice.MarketDataService; import org.marketcetera.marketdata.core.webservice.PageRequest; import org.marketcetera.marketdata.core.webservice.UnknownRequestException; import org.marketcetera.trade.Instrument; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.ClassVersion; import org.marketcetera.util.ws.stateful.ClientContext; import org.marketcetera.util.ws.stateful.RemoteCaller; import org.marketcetera.util.ws.stateful.ServerProvider; import org.marketcetera.util.ws.stateful.ServiceBaseImpl; import org.marketcetera.util.ws.stateful.SessionHolder; import org.marketcetera.util.ws.stateful.SessionManager; import org.marketcetera.util.ws.stateless.ServiceInterface; import org.marketcetera.util.ws.wrappers.RemoteException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.Lifecycle; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /* $License$ */ /** * Provides Market Data Nexus services. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketDataServiceImpl.java 16901 2014-05-11 16:14:11Z colin $ * @since 2.4.0 */ @ThreadSafe @ClassVersion("$Id: MarketDataServiceImpl.java 16901 2014-05-11 16:14:11Z colin $") public class MarketDataServiceImpl extends ServiceBaseImpl<Object> implements MarketDataService,Lifecycle,MarketDataServiceAdapter { /** * Create a new MarketDataWebServiceImpl instance. * * @param inSessionManager a <code>SessionManager<Object></code> value */ public MarketDataServiceImpl(SessionManager<Object> inSessionManager) { super(inSessionManager); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#getAvailableCapability(org.marketcetera.util.ws.stateful.ClientContext) */ @Override public Set<Capability> getAvailableCapability(ClientContext inContext) throws RemoteException { return new RemoteCaller<Object,Set<Capability>>(getSessionManager()) { @Override protected Set<Capability> call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting market data capabilities", inContext.getSessionId()); checkConnection(); return marketDataManager.getAvailableCapability(); } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#getAvailableCapability() */ @Override public Set<Capability> getAvailableCapability() { return marketDataManager.getAvailableCapability(); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#request(org.marketcetera.util.ws.stateful.ClientContext, org.marketcetera.marketdata.MarketDataRequest, boolean) */ @Override public long request(ClientContext inContext, final MarketDataRequest inRequest, final boolean inStreamEvents) throws RemoteException { return new RemoteCaller<Object,Long>(getSessionManager()) { @Override protected Long call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting {}", inContext.getSessionId(), inRequest); checkConnection(); long requestId = doRequest(inRequest, inStreamEvents); SLF4JLoggerProxy.debug(this, "{} returning {} for {}", inContext, requestId, inRequest); return requestId; } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#request(org.marketcetera.marketdata.MarketDataRequest, boolean) */ @Override public long request(MarketDataRequest inRequest, boolean inStreamEvents) { return doRequest(inRequest, inStreamEvents); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#getAllEvents(org.marketcetera.util.ws.stateful.ClientContext, java.util.List) */ @Override public Map<Long,LinkedList<Event>> getAllEvents(ClientContext inContext, final List<Long> inRequestIds) throws RemoteException { return new RemoteCaller<Object,Map<Long,LinkedList<Event>>>(getSessionManager()) { @Override protected Map<Long,LinkedList<Event>> call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting events for {}", inContext.getSessionId(), inRequestIds); checkConnection(); Map<Long,LinkedList<Event>> eventsToReturn = Maps.newLinkedHashMap(); for(Long requestId : inRequestIds) { LinkedList<Event> events = Lists.newLinkedList(doGetEvents(requestId)); eventsToReturn.put(requestId, events); } return eventsToReturn; } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#getAllEvents(java.util.List) */ @Override public Map<Long,LinkedList<Event>> getAllEvents(List<Long> inRequestIds) { Map<Long,LinkedList<Event>> eventsToReturn = Maps.newLinkedHashMap(); for(Long requestId : inRequestIds) { LinkedList<Event> events = Lists.newLinkedList(doGetEvents(requestId)); eventsToReturn.put(requestId, events); } return eventsToReturn; } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataWebService#getEvents(org.marketcetera.util.ws.stateful.ClientContext, long) */ @Override public Deque<Event> getEvents(ClientContext inContext, final long inRequestId) throws RemoteException { return new RemoteCaller<Object,Deque<Event>>(getSessionManager()) { @Override protected Deque<Event> call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting events for {}", inContext.getSessionId(), inRequestId); checkConnection(); Deque<Event> eventsToReturn = doGetEvents(inRequestId); SLF4JLoggerProxy.debug(this, "{} returning {} for {}", inContext.getSessionId(), eventsToReturn, inRequestId); return eventsToReturn; } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#getEvents(long) */ @Override public Deque<Event> getEvents(long inRequestId) { return doGetEvents(inRequestId); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#getLastUpdate(org.marketcetera.util.ws.stateful.ClientContext, long) */ @Override public long getLastUpdate(ClientContext inContext, final long inRequestId) throws RemoteException { return new RemoteCaller<Object,Long>(getSessionManager()) { @Override protected Long call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting update timestamp for {}", inContext.getSessionId(), inRequestId); checkConnection(); long timestamp = doGetLastUpdate(inRequestId); SLF4JLoggerProxy.debug(this, "{} returning {} for {}", inContext.getSessionId(), timestamp, inRequestId); return timestamp; } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#getLastUpdate(long) */ @Override public long getLastUpdate(long inId) { return doGetLastUpdate(inId); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#heartbeat(org.marketcetera.util.ws.stateful.ClientContext) */ @Override public void heartbeat(ClientContext inContext) throws RemoteException { new RemoteCaller<Object,Void>(getSessionManager()) { @Override protected Void call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { return null; } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#getSnapshot(org.marketcetera.util.ws.stateful.ClientContext, org.marketcetera.trade.Instrument, org.marketcetera.marketdata.Content, java.lang.String) */ @Override public Deque<Event> getSnapshot(ClientContext inContext, final Instrument inInstrument, final Content inContent, final String inProvider) throws RemoteException { return new RemoteCaller<Object,Deque<Event>>(getSessionManager()) { @Override protected Deque<Event> call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting snapshot for {} {} from {}", inContext.getSessionId(), inContent, inInstrument, inProvider); checkConnection(); return doGetSnapshot(inInstrument, inContent, inProvider); } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#getSnapshot(org.marketcetera.trade.Instrument, org.marketcetera.marketdata.Content, java.lang.String) */ @Override public Deque<Event> getSnapshot(Instrument inInstrument, Content inContent, String inProvider) { return doGetSnapshot(inInstrument, inContent, inProvider); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataService#getSnapshotPage(org.marketcetera.util.ws.stateful.ClientContext, org.marketcetera.trade.Instrument, org.marketcetera.marketdata.Content, java.lang.String, org.springframework.data.domain.PageRequest) */ @Override public Deque<Event> getSnapshotPage(ClientContext inContext, final Instrument inInstrument, final Content inContent, final String inProvider, final PageRequest inPage) throws RemoteException { return new RemoteCaller<Object,Deque<Event>>(getSessionManager()) { @Override protected Deque<Event> call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { SLF4JLoggerProxy.debug(this, "{} requesting snapshot page {} for {} {} from {}", inContext.getSessionId(), inPage, inContent, inInstrument, inProvider); checkConnection(); return doGetSnapshotPage(inInstrument, inContent, inProvider, inPage); } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#getSnapshotPage(org.marketcetera.trade.Instrument, org.marketcetera.marketdata.Content, java.lang.String, org.marketcetera.marketdata.core.webservice.PageRequest) */ @Override public Deque<Event> getSnapshotPage(Instrument inInstrument, Content inContent, String inProvider, PageRequest inPageRequest) { return doGetSnapshotPage(inInstrument, inContent, inProvider, inPageRequest); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.webservice.MarketDataWebService#cancel(org.marketcetera.util.ws.stateful.ClientContext, long) */ @Override public void cancel(ClientContext inContext, final long inRequestId) throws RemoteException { new RemoteCaller<Object,Void>(getSessionManager()){ @Override protected Void call(ClientContext inContext, SessionHolder<Object> inSessionHolder) throws Exception { doCancel(inRequestId); return null; } }.execute(inContext); } /* (non-Javadoc) * @see org.marketcetera.marketdata.core.rpc.MarketDataServiceAdapter#cancel(long) */ @Override public void cancel(long inRequestId) { doCancel(inRequestId); } /* (non-Javadoc) * @see org.springframework.context.Lifecycle#isRunning() */ @Override public boolean isRunning() { return running.get(); } /* (non-Javadoc) * @see org.springframework.context.Lifecycle#start() */ @Override public void start() { if(isRunning()) { stop(); } Validate.notNull(marketDataManager); Validate.notNull(serverProvider); reaper = Executors.newScheduledThreadPool(1); reaper.scheduleAtFixedRate(new Reaper(), reaperInterval, reaperInterval, TimeUnit.MILLISECONDS); remoteService = serverProvider.getServer().publish(this, MarketDataService.class); running.set(true); } /* (non-Javadoc) * @see org.springframework.context.Lifecycle#stop() */ @Override public void stop() { if(reaper != null) { reaper.shutdownNow(); reaper = null; } try { remoteService.stop(); } catch (RuntimeException ignored) { } finally { remoteService = null; subscribersByRequestId.clear(); running.set(false); } } /** * Get the server value. * * @return a <code>ServerProvider<?></code> value */ public ServerProvider<?> getServer() { return serverProvider; } /** * Sets the server value. * * @param inServer a <code>ServerProvider<?></code> value */ public void setServer(ServerProvider<?> inServer) { serverProvider = inServer; } /** * Get the marketDataManager value. * * @return a <code>MarketDataManager</code> value */ public MarketDataManager getMarketDataManager() { return marketDataManager; } /** * Sets the marketDataManager value. * * @param inMarketDataManager a <code>MarketDataManager</code> value */ public void setMarketDataManager(MarketDataManager inMarketDataManager) { marketDataManager = inMarketDataManager; } /** * Get the reaperInterval value. * * @return a <code>long</code> value */ public long getReaperInterval() { return reaperInterval; } /** * Sets the reaperInterval value. * * @param inReaperInterval a <code>long</code> value */ public void setReaperInterval(long inReaperInterval) { reaperInterval = inReaperInterval; } /** * Get the maxSubscriptionInterval value. * * @return a <code>long</code> value */ public long getMaxSubscriptionInterval() { return maxSubscriptionInterval; } /** * Sets the maxSubscriptionInterval value. * * @param inMaxSubscriptionInterval a <code>long</code> value */ public void setMaxSubscriptionInterval(long inMaxSubscriptionInterval) { maxSubscriptionInterval = inMaxSubscriptionInterval; } /** * Executes the given market data request. * * @param inRequest a <code>MarketDataRequest</code> value * @param inStreamEvents a <code>boolean</code> value * @return a <code>long</code> value */ private long doRequest(MarketDataRequest inRequest, boolean inStreamEvents) { ServiceSubscriber subscriber = new ServiceSubscriber(inStreamEvents); long requestId = marketDataManager.requestMarketData(inRequest, subscriber); subscriber.setRequestId(requestId); subscribersByRequestId.put(requestId, subscriber); return requestId; } /** * Executes a get last update call. * * @param inRequestId a <code>long</code> value * @return a <code>long</code> value */ private long doGetLastUpdate(long inRequestId) { ServiceSubscriber subscriber = subscribersByRequestId.get(inRequestId); if(subscriber == null) { throw new UnknownRequestException(inRequestId); } long timestamp = subscriber.getUpdateTimestamp(); return timestamp; } /** * Retrieves the events for the given request. * * @param inRequestId a <code>long</code> value * @return a <code>Deque<Event></code> value * @throws UnknownRequestException if the given request is invalid */ private Deque<Event> doGetEvents(long inRequestId) { ServiceSubscriber subscriber = subscribersByRequestId.get(inRequestId); if(subscriber == null) { throw new UnknownRequestException(inRequestId); } return subscriber.getEvents(); } /** * Cancels the market data request with the given id. * * @param inRequestId a <code>long</code> value */ private void doCancel(long inRequestId) { ServiceSubscriber subscriber = subscribersByRequestId.remove(inRequestId); if(subscriber != null) { marketDataManager.cancelMarketDataRequest(inRequestId); subscriber.cancel(); } } /** * Gets the most recent snapshot for the given attributes. * * @param inInstrument an <code>Instrument</code> value * @param inContent a <code>Content</code> value * @param inProvider a <code>String</code> value or <code>null</code> * @return a <code>Deque<Event></code> value */ private Deque<Event> doGetSnapshot(Instrument inInstrument, Content inContent, String inProvider) { Event event = marketDataManager.requestMarketDataSnapshot(inInstrument, inContent, inProvider); if(event == null) { return new LinkedList<>(); } Deque<Event> eventsToReturn = Lists.newLinkedList(); if(event instanceof AggregateEvent) { eventsToReturn.addAll(((AggregateEvent)event).decompose()); } else { eventsToReturn.add(event); } return eventsToReturn; } /** * Gets the requested page of the most recent snapshot for the given attribute. * * @param inInstrument an <code>Instrument</code> value * @param inContent a <code>Content</code> value * @param inProvider a <code>String</code> value or <code>null</code> * @param inPageRequest a <code>PageRequest</code> value * @return a <code>Deque<Event></code> value */ private Deque<Event> doGetSnapshotPage(Instrument inInstrument, Content inContent, String inProvider, PageRequest inPageRequest) { Event event = marketDataManager.requestMarketDataSnapshot(inInstrument, inContent, inProvider); if(event == null) { return new LinkedList<>(); } Deque<Event> eventsToReturn = Lists.newLinkedList(); if(event instanceof AggregateEvent) { eventsToReturn.addAll(((AggregateEvent)event).decompose()); } else { eventsToReturn.add(event); } // TODO pick out page return eventsToReturn; } /** * Checks that the connection is active. * * @throws ConnectionException if the connection is not active */ private void checkConnection() { if(!isRunning()) { throw new ConnectionException(); } } /** * Manages a request subscription. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketDataServiceImpl.java 16901 2014-05-11 16:14:11Z colin $ * @since 2.4.0 */ @ThreadSafe @ClassVersion("$Id: MarketDataServiceImpl.java 16901 2014-05-11 16:14:11Z colin $") private class ServiceSubscriber implements ISubscriber { /** * Create a new ServiceSubscriber instance. * * @param inStreamEvents a <code>boolean</code> value */ public ServiceSubscriber(boolean inStreamEvents) { storeEvents = inStreamEvents; } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#isInteresting(java.lang.Object) */ @Override public boolean isInteresting(Object inData) { return true; } /* (non-Javadoc) * @see org.marketcetera.core.publisher.ISubscriber#publishTo(java.lang.Object) */ @Override public void publishTo(Object inData) { try(CloseableLock publishEventLock = CloseableLock.create(lock.writeLock())) { publishEventLock.lock(); updateTimestamp = System.currentTimeMillis(); if(!storeEvents) { return; } if(inData instanceof Event) { events.addFirst((Event)inData); } else if(inData instanceof AggregateEvent) { for(Event event : ((AggregateEvent)inData).decompose()) { events.addFirst(event); } } else if(inData instanceof Collection<?>) { Collection<?> collectionData = (Collection<?>)inData; for(Object data : collectionData) { publishTo(data); } } else { SLF4JLoggerProxy.warn(this, "Unknown data type: " + inData.getClass().getName()); // TODO message throw new UnsupportedOperationException(); } } } /** * Performs the actions necessary to clean up this subscriber when it is no longer needed. */ private void cancel() { try(CloseableLock publishEventLock = CloseableLock.create(lock.writeLock())) { publishEventLock.lock(); events.clear(); } } /** * Get the events value. * * @return a <code>Deque<Event></code> value */ private Deque<Event> getEvents() { retrieveTimestamp = System.currentTimeMillis(); Deque<Event> eventsToReturn = Lists.newLinkedList(); try(CloseableLock getEventLock = CloseableLock.create(lock.writeLock())) { getEventLock.lock(); eventsToReturn.addAll(events); events.clear(); } return eventsToReturn; } /** * Get the updateTimestamp value. * * @return a <code>long</code> value */ private long getUpdateTimestamp() { return updateTimestamp; } /** * Get the requestId value. * * @return a <code>long</code> value */ private long getRequestId() { return requestId; } /** * Sets the requestId value. * * @param inRequestId a <code>long</code> value */ private void setRequestId(long inRequestId) { requestId = inRequestId; } /** * market data request id */ private volatile long requestId; /** * last time this subscription was harvested */ private volatile long retrieveTimestamp = System.currentTimeMillis(); /** * indicates whether the subscriber should store updates or not */ private final boolean storeEvents; /** * tracks the time of the most recent update to the subscription */ private volatile long updateTimestamp; /** * controls access to critical data */ private final ReadWriteLock lock = new ReentrantReadWriteLock(); /** * contains events not yet seen for this subscriber */ @GuardedBy("lock") private final Deque<Event> events = Lists.newLinkedList(); } /** * Retires market data subscriptions that have not been checked in a while. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: MarketDataServiceImpl.java 16901 2014-05-11 16:14:11Z colin $ * @since 2.4.0 */ @ClassVersion("$Id: MarketDataServiceImpl.java 16901 2014-05-11 16:14:11Z colin $") private class Reaper implements Runnable { /* (non-Javadoc) * @see java.lang.Runnable#run() */ @Override public void run() { try { // we're accessing this list concurrently, so the contents might change. let's use a copy of the list Collection<ServiceSubscriber> subscribers = Lists.newArrayList(subscribersByRequestId.values()); SLF4JLoggerProxy.debug(MarketDataServiceImpl.this, "Reaper examining {} subscription(s)", subscribers.size()); for(ServiceSubscriber subscriber : subscribers) { if(subscriber.storeEvents && subscriber.retrieveTimestamp < System.currentTimeMillis()-maxSubscriptionInterval) { SLF4JLoggerProxy.debug(MarketDataServiceImpl.this, "Reaper canceling {}", subscriber); doCancel(subscriber.getRequestId()); } } } catch (Exception e) { SLF4JLoggerProxy.warn(MarketDataServiceImpl.this, e); } } } /** * interval at which subscriptions are checked */ private long reaperInterval = 10000; /** * max life of a subscription that has not been harvested */ private long maxSubscriptionInterval = 10000; /** * executes repear jobs */ private ScheduledExecutorService reaper; /** * handle to the remote web service. */ private ServiceInterface remoteService; /** * provides the running web server to publish this service on */ @Autowired private ServerProvider<?> serverProvider; /** * provides access to market data services */ @Autowired private MarketDataManager marketDataManager; /** * tracks subscribers by request id */ private final Map<Long,ServiceSubscriber> subscribersByRequestId = Maps.newConcurrentMap(); /** * indicates if the service is running or not */ private final AtomicBoolean running = new AtomicBoolean(false); }