/**
* 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.realtime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.onebusaway.collections.ConcurrentCollectionsLibrary;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.realtime.api.VehicleLocationRecord;
import org.onebusaway.transit_data_federation.services.blocks.BlockInstance;
import org.onebusaway.transit_data_federation.services.blocks.ScheduledBlockLocation;
import org.onebusaway.transit_data_federation.services.realtime.ScheduleDeviationSamples;
import org.onebusaway.transit_data_federation.services.realtime.VehicleLocationCacheElements;
import org.onebusaway.transit_data_federation.services.realtime.VehicleLocationCacheEntry;
import org.onebusaway.transit_data_federation.services.realtime.VehicleLocationRecordCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* The general assumption is that we won't get back to back updates from the
* same vehicle on a short enough time-scale that it can cause serious race
* condition issues. If we DO get lots of back to back updates, we'll just have
* to assume some amount of data loss.
*
* @param record record to add
*/
@Component
class VehicleLocationRecordCacheImpl implements VehicleLocationRecordCache {
private static Logger _log = LoggerFactory.getLogger(VehicleLocationRecordCacheImpl.class);
private ConcurrentMap<AgencyAndId, VehicleLocationCacheEntry> _entriesByVehicleId = new ConcurrentHashMap<AgencyAndId, VehicleLocationCacheEntry>();
private ConcurrentMap<BlockInstance, Set<AgencyAndId>> _vehicleIdsByBlockInstance = new ConcurrentHashMap<BlockInstance, Set<AgencyAndId>>();
/**
* By default, we keep around 20 minutes of cache entries
*/
private int _blockLocationRecordCacheWindowSize = 20 * 60;
private int _cacheEvictionFrequency = 1;
private ScheduledExecutorService _executor;
private ScheduledFuture<?> _evictionHandler;
/**
* Controls how far back in time we include records in the
* {@link BlockLocationRecordCollection} for each active trip.
*
* @param windowSize in seconds
*/
public void setBlockLocationRecordCacheWindowSize(int windowSize) {
_blockLocationRecordCacheWindowSize = windowSize;
}
/**
*
* @param cacheEvictionFrequency frequency, in minutes
*/
public void setCacheEvictionFrequency(int cacheEvictionFrequency) {
_cacheEvictionFrequency = cacheEvictionFrequency;
}
@PostConstruct
public void start() {
_executor = Executors.newScheduledThreadPool(1);
_evictionHandler = _executor.scheduleAtFixedRate(
new CacheEvictionHandler(), _cacheEvictionFrequency,
_cacheEvictionFrequency, TimeUnit.MINUTES);
}
@PreDestroy
public void stop() {
if (_evictionHandler != null)
_evictionHandler.cancel(true);
if (_executor != null)
_executor.shutdownNow();
}
/****
* {@link VehicleLocationRecordCache} Interface
****/
@Override
public VehicleLocationCacheElements getRecordForVehicleId(
AgencyAndId vehicleId) {
VehicleLocationCacheEntry entry = _entriesByVehicleId.get(vehicleId);
if (entry == null)
return null;
return entry.getElements();
}
@Override
public List<VehicleLocationCacheElements> getRecordsForBlockInstance(
BlockInstance blockInstance) {
Set<AgencyAndId> vehicleIds = _vehicleIdsByBlockInstance.get(blockInstance);
List<VehicleLocationCacheElements> records = new ArrayList<VehicleLocationCacheElements>();
if (vehicleIds != null) {
for (AgencyAndId vehicleId : vehicleIds) {
VehicleLocationCacheEntry record = _entriesByVehicleId.get(vehicleId);
if (record != null && record.getBlockInstance().equals(blockInstance))
records.add(record.getElements());
}
}
return records;
}
@Override
public VehicleLocationCacheElements addRecord(BlockInstance blockInstance,
VehicleLocationRecord record,
ScheduledBlockLocation scheduledBlockLocation,
ScheduleDeviationSamples samples) {
AgencyAndId vehicleId = record.getVehicleId();
while (true) {
VehicleLocationCacheEntry newCacheEntry = new VehicleLocationCacheEntry(
blockInstance);
VehicleLocationCacheEntry cacheEntry = _entriesByVehicleId.putIfAbsent(
vehicleId, newCacheEntry);
if (cacheEntry == null) {
cacheEntry = newCacheEntry;
/**
* Since we're adding a new entry, we indicate the connection between
* this block instance and vehicleId
*/
ConcurrentCollectionsLibrary.addToMapValueSet(
_vehicleIdsByBlockInstance, blockInstance, vehicleId);
}
/**
* If the block instance of a vehicle has changed mid-stream, we close off
* the cache entry and remove the block=>vid mapping
*/
if (cacheEntry.isClosedBecauseBlockInstanceChanged(blockInstance)) {
_entriesByVehicleId.remove(vehicleId);
ConcurrentCollectionsLibrary.removeFromMapValueSet(
_vehicleIdsByBlockInstance, cacheEntry.getBlockInstance(),
vehicleId);
continue;
}
/**
* If the element failed to add because the entry is closed, we loop.
* Someone closed the entry while we were in the process of requesting it
* from the map. On the next loop, it should no longer be in the map.
*/
if (!cacheEntry.addElement(record, scheduledBlockLocation, samples))
continue;
BlockInstance existingBlockInstance = cacheEntry.getBlockInstance();
if (!blockInstance.equals(existingBlockInstance))
ConcurrentCollectionsLibrary.removeFromMapValueSet(
_vehicleIdsByBlockInstance, existingBlockInstance, vehicleId);
// Ensure the block => vehicle mapping is set
return cacheEntry.getElements();
}
}
@Override
public void clearRecordsForVehicleId(AgencyAndId vehicleId) {
VehicleLocationCacheEntry record = _entriesByVehicleId.remove(vehicleId);
if (record != null) {
ConcurrentCollectionsLibrary.removeFromMapValueSet(
_vehicleIdsByBlockInstance, record.getBlockInstance(), vehicleId);
}
}
public void clearStaleRecords(long time) {
Iterator<Entry<AgencyAndId, VehicleLocationCacheEntry>> it = _entriesByVehicleId.entrySet().iterator();
while (it.hasNext()) {
Entry<AgencyAndId, VehicleLocationCacheEntry> entry = it.next();
AgencyAndId vehicleId = entry.getKey();
VehicleLocationCacheEntry cacheEntry = entry.getValue();
if (cacheEntry.closeIfStale(time)) {
if (_log.isDebugEnabled())
_log.debug("pruning block location record cache for vehicle="
+ vehicleId + " block=" + cacheEntry.getBlockInstance());
it.remove();
ConcurrentCollectionsLibrary.removeFromMapValueSet(
_vehicleIdsByBlockInstance, cacheEntry.getBlockInstance(),
vehicleId);
}
}
}
/****
* Private Methods
****/
private class CacheEvictionHandler implements Runnable {
@Override
public void run() {
clearStaleRecords(System.currentTimeMillis()
- _blockLocationRecordCacheWindowSize * 1000);
}
}
}