/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * * 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.onebusaway.transit_data_federation.impl; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.onebusaway.exceptions.ServiceException; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.transit_data.model.RegisterAlarmQueryBean; import org.onebusaway.transit_data_federation.services.AgencyAndIdLibrary; import org.onebusaway.transit_data_federation.services.AlarmAction; import org.onebusaway.transit_data_federation.services.ArrivalAndDepartureAlarmService; import org.onebusaway.transit_data_federation.services.ArrivalAndDepartureQuery; import org.onebusaway.transit_data_federation.services.ArrivalAndDepartureService; import org.onebusaway.transit_data_federation.services.blocks.BlockInstance; import org.onebusaway.transit_data_federation.services.realtime.ArrivalAndDepartureInstance; import org.onebusaway.transit_data_federation.services.realtime.BlockLocation; import org.onebusaway.transit_data_federation.services.realtime.BlockLocationListener; import org.onebusaway.transit_data_federation.services.transit_graph.StopEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component class ArrivalAndDepartureAlarmServiceImpl implements ArrivalAndDepartureAlarmService, BlockLocationListener { private static Logger _log = LoggerFactory.getLogger(ArrivalAndDepartureAlarmServiceImpl.class); private ArrivalAndDepartureService _arrivalAndDepartureService; private ConcurrentMap<BlockInstance, AlarmsForBlockInstance> _alarmsByBlockInstance = new ConcurrentHashMap<BlockInstance, AlarmsForBlockInstance>(); private Map<AgencyAndId, AlarmForBlockInstance> _alarmsById = new HashMap<AgencyAndId, AlarmForBlockInstance>(); private ScheduledExecutorService _executor; private int _threadPoolSize = 5; @Autowired public void setArrivalAndDepartureService( ArrivalAndDepartureService arrivalAndDepartureService) { _arrivalAndDepartureService = arrivalAndDepartureService; } public void setThreadPoolSize(int threadPoolSize) { _threadPoolSize = threadPoolSize; } /**** * ****/ @PostConstruct public void start() { _executor = Executors.newScheduledThreadPool(_threadPoolSize); } @PreDestroy public void stop() { if (_executor != null) { _executor.shutdownNow(); _executor = null; } } /**** * {@link ArrivalAndDepartureAlarmService} Interface ****/ @Override public AgencyAndId registerAlarmForArrivalAndDepartureAtStop( ArrivalAndDepartureQuery query, RegisterAlarmQueryBean alarmBean) { ArrivalAndDepartureInstance instance = _arrivalAndDepartureService.getArrivalAndDepartureForStop(query); if (instance == null) throw new ServiceException("no arrival-departure found"); /** * We group alarms by block instance */ BlockInstance blockInstance = instance.getBlockInstance(); /** * Retrieve the alarms for the block instance */ AlarmsForBlockInstance alarms = getAlarmsForBlockInstance(blockInstance); /** * The effective schedule time is the point in the transit vehicle's * schedule run time when the alarm should be fired */ int effectiveScheduleTime = computeEffectiveScheduleTimeForAlarm(alarmBean, instance); /** * Create and register the alarm */ AlarmAction action = new AlarmAction(); action.setUrl(alarmBean.getUrl()); AlarmForBlockInstance alarm = alarms.registerAlarm(action, effectiveScheduleTime, instance); _alarmsById.put(alarm.getId(), alarm); _log.debug("alarm created: {}", alarm.getId()); return alarm.getId(); } @Override public void cancelAlarmForArrivalAndDepartureAtStop(AgencyAndId alarmId) { _log.debug("cancelling alarm: {}", alarmId); AlarmForBlockInstance alarm = _alarmsById.get(alarmId); if (alarm != null) alarm.setCanceled(); } /**** * {@link BlockLocationListener} Interface ****/ @Override public void handleBlockLocation(BlockLocation blockLocation) { if (blockLocation == null) return; /** * If we have new real-time info for a block, we need to update the alarms * attached to the block instance */ BlockInstance blockInstance = blockLocation.getBlockInstance(); AlarmsForBlockInstance alarms = _alarmsByBlockInstance.get(blockInstance); if (alarms != null) alarms.updateBlockLocation(blockLocation); } /**** * Private Methods ****/ /** * The effective schedule time is the point in the transit vehicle's schedule * run time when the alarm should be fired. It's determined by the scheduled * departure or arrival time, adjusted by the alarm offset time */ private int computeEffectiveScheduleTimeForAlarm( RegisterAlarmQueryBean alarmBean, ArrivalAndDepartureInstance instance) { long scheduleTime = alarmBean.isOnArrival() ? instance.getScheduledArrivalTime() : instance.getScheduledDepartureTime(); int effectiveScheduleTime = (int) ((scheduleTime - instance.getServiceDate()) / 1000); return effectiveScheduleTime - alarmBean.getAlarmTimeOffset(); } /** * We group the alarms by their block instance, storing the alarms in a * ConcurrentMap keyed off the block instance. When all alarms for a block * instance have been fired, we'd like to be able to clean and remove the * alarms object. However, there is a possibility for a race condition when * attempting to remove the AlarmsForBlockInstance object from the concurrent * map when another alarm is being registered at the same time. We get around * this by marking an alarms object as "canceled", indicating that no new * alarms can be registered. Thus, we loop while until we get an active alarm * instance. * * @param blockInstance * @return */ private AlarmsForBlockInstance getAlarmsForBlockInstance( BlockInstance blockInstance) { while (true) { AlarmsForBlockInstance alarms = _alarmsByBlockInstance.get(blockInstance); if (alarms == null) { AlarmsForBlockInstance newAlarms = new AlarmsForBlockInstance( blockInstance); alarms = _alarmsByBlockInstance.putIfAbsent(blockInstance, newAlarms); if (alarms == null) alarms = newAlarms; } if (alarms.isCanceled()) continue; return alarms; } } private void fireAlarm(AlarmForBlockInstance alarm) { _executor.submit(new FireAlarmTask(alarm.getId(), alarm.action)); } /**** * ****/ private class AlarmsForBlockInstance implements Runnable { private final BlockInstance _blockInstance; /** * Queue of alarms where no real-time data is available. If real-time * becomes available, we'll upgrade the alarm to the first real-time-equiped * vehicle. */ private PriorityQueue<AlarmForBlockInstance> _noVehicleIdQueue = new PriorityQueue<AlarmForBlockInstance>(); /** * Queues of alarms grouped by vehicle id. Remember that multiple vehicles * can be servicing the same block instance. */ private Map<AgencyAndId, VehicleInfo> _vehicleInfoByVehicleId = new HashMap<AgencyAndId, VehicleInfo>(); /** * The actual task that will run to check and fire alarms in the future. We * reschedule this task to reflect the next upcoming alarm. */ private Future<?> _alarmTask = null; /** * Indicates that this alarms instance has been canceled and no new alarms * should be registered. An alarm instance is canceled when it contains no * new alarms. The "canceled" flag helps avoid a race condition where * additional alarms are added while we are in the process of cleanup. */ private boolean _canceled = false; public AlarmsForBlockInstance(BlockInstance blockInstance) { _blockInstance = blockInstance; } public synchronized boolean isCanceled() { return _canceled; } public synchronized AlarmForBlockInstance registerAlarm(AlarmAction action, int effectiveScheduleTime, ArrivalAndDepartureInstance instance) { StopEntry stop = instance.getStop(); AgencyAndId stopId = stop.getId(); AgencyAndId alarmId = new AgencyAndId(stopId.getAgencyId(), UUID.randomUUID().toString()); AlarmForBlockInstance alarm = new AlarmForBlockInstance(alarmId, action, effectiveScheduleTime); /** * We put the alarm in the schedule-only vs real-time queue as appropriate */ BlockLocation blockLocation = instance.getBlockLocation(); if (blockLocation == null || blockLocation.getVehicleId() == null) { _log.debug("schedule only for alarm: {}", instance); _noVehicleIdQueue.add(alarm); } else { _log.debug("real-time for alarm: {}", instance); AgencyAndId vehicleId = blockLocation.getVehicleId(); VehicleInfo vehicleInfo = getVehicleInfoForVehicleId(vehicleId, true); if (blockLocation.isScheduleDeviationSet()) vehicleInfo.setScheduleDeviation((int) blockLocation.getScheduleDeviation()); else _log.warn("no schedule deviation for block location " + blockLocation); PriorityQueue<AlarmForBlockInstance> queue = vehicleInfo.getQueue(); queue.add(alarm); } processQueues(); return alarm; } public synchronized void updateBlockLocation(BlockLocation blockLocation) { AgencyAndId vehicleId = blockLocation.getVehicleId(); if (vehicleId == null) { _log.warn("expected a vehicle id with block location" + blockLocation); return; } if (!blockLocation.isScheduleDeviationSet()) { _log.warn("expected schedule deviation with block location" + blockLocation); } _log.debug("updating block location for vehicle: {}", blockLocation.getVehicleId()); /** * We've create the vehicle info queue if it means we can move alarms out * of the "scheduled arrival" queue */ boolean create = !_noVehicleIdQueue.isEmpty(); VehicleInfo vehicleInfo = getVehicleInfoForVehicleId(vehicleId, create); if (vehicleInfo == null) return; vehicleInfo.setScheduleDeviation((int) blockLocation.getScheduleDeviation()); moveNoVehicleAlarmsToVehicleAlarms(); processQueues(); } /** * This is called by the scheduler */ @Override public synchronized void run() { _alarmTask = null; processQueues(); } /**** * ****/ private VehicleInfo getVehicleInfoForVehicleId(AgencyAndId vehicleId, boolean create) { VehicleInfo vehicleInfo = _vehicleInfoByVehicleId.get(vehicleId); if (vehicleInfo == null && create) { vehicleInfo = new VehicleInfo(); _vehicleInfoByVehicleId.put(vehicleId, vehicleInfo); } return vehicleInfo; } /** * If we had alarms set for a "scheduled arrival" and we now have real-time * tracking for a vehicle serving that arrival, we move the alarms over. */ private void moveNoVehicleAlarmsToVehicleAlarms() { if (_noVehicleIdQueue.isEmpty() || _vehicleInfoByVehicleId.isEmpty()) return; VehicleInfo first = _vehicleInfoByVehicleId.values().iterator().next(); PriorityQueue<AlarmForBlockInstance> queue = first.getQueue(); queue.addAll(_noVehicleIdQueue); _noVehicleIdQueue.clear(); } private void processQueues() { if (_alarmTask != null) _alarmTask.cancel(false); boolean allQueuesAreEmpty = true; int minNextAlarmTime = Integer.MAX_VALUE; for (VehicleInfo vehicleInfo : _vehicleInfoByVehicleId.values()) { PriorityQueue<AlarmForBlockInstance> queue = vehicleInfo.getQueue(); int scheduleDeviation = vehicleInfo.getScheduleDeviation(); int nextAlarmTime = processQueue(queue, scheduleDeviation); if (nextAlarmTime > 0) { minNextAlarmTime = Math.min(minNextAlarmTime, nextAlarmTime); allQueuesAreEmpty = false; } } int nextAlarmTime = processQueue(_noVehicleIdQueue, 0); if (nextAlarmTime > 0) { minNextAlarmTime = Math.min(minNextAlarmTime, nextAlarmTime); allQueuesAreEmpty = false; } if (allQueuesAreEmpty) { _log.debug("all alarm queues are empty, cleaning up: {}", _blockInstance); _vehicleInfoByVehicleId.clear(); _canceled = true; _alarmsByBlockInstance.remove(_blockInstance); } else { _log.debug("scheduling next alarm check in {} secs for {}", minNextAlarmTime, _blockInstance); /** * Schedule the next alarm */ _alarmTask = _executor.schedule(this, minNextAlarmTime, TimeUnit.SECONDS); } } private int processQueue(PriorityQueue<AlarmForBlockInstance> queue, int scheduleDeviation) { int effectiveScheduleTime = (int) ((System.currentTimeMillis() - _blockInstance.getServiceDate()) / 1000 - scheduleDeviation); while (!queue.isEmpty()) { AlarmForBlockInstance alarm = queue.peek(); if (alarm.isCanceled()) { queue.poll(); continue; } /** * If the first alarm in the queue isn't ready to be fired yet, we * return the time until it should be fired */ if (effectiveScheduleTime < alarm.getEffectiveScheduleTime()) { return alarm.getEffectiveScheduleTime() - effectiveScheduleTime; } queue.poll(); fireAlarm(alarm); } /** * We've gone through all the alarms */ return -1; } } private class VehicleInfo { private final PriorityQueue<AlarmForBlockInstance> _queue = new PriorityQueue<AlarmForBlockInstance>(); private int _scheduleDeviation = 0; public int getScheduleDeviation() { return _scheduleDeviation; } public void setScheduleDeviation(int scheduleDeviation) { _scheduleDeviation = scheduleDeviation; } public PriorityQueue<AlarmForBlockInstance> getQueue() { return _queue; } } private class AlarmForBlockInstance implements Comparable<AlarmForBlockInstance> { private final AgencyAndId id; private final AlarmAction action; private final int effectiveScheduleTime; private boolean canceled = false; public AlarmForBlockInstance(AgencyAndId id, AlarmAction action, int effectiveScheduleTime) { this.id = id; this.action = action; this.effectiveScheduleTime = effectiveScheduleTime; } public AgencyAndId getId() { return id; } public int getEffectiveScheduleTime() { return effectiveScheduleTime; } public void setCanceled() { canceled = true; } public boolean isCanceled() { return canceled; } @Override public int compareTo(AlarmForBlockInstance o) { return this.effectiveScheduleTime - o.effectiveScheduleTime; } } /** * This task encapsulates the task of actually executing an alarm so that it * can be executed asynchronously * * @author bdferris * */ private static class FireAlarmTask implements Runnable { private final AgencyAndId alarmId; private final AlarmAction action; public FireAlarmTask(AgencyAndId alarmId, AlarmAction action) { this.alarmId = alarmId; this.action = action; } @Override public void run() { try { String rawUrl = action.getUrl(); String rawAlarmId = AgencyAndIdLibrary.convertToString(alarmId); rawUrl = rawUrl.replace("#ALARM_ID#", rawAlarmId); URL url = new URL(rawUrl); URLConnection connection = url.openConnection(); InputStream in = connection.getInputStream(); in.close(); } catch (Throwable ex) { _log.warn("error firing alarm", ex); } } } }