/***************************************************************************
* Copyright (C) 2011 by H-Store Project *
* Brown University *
* Massachusetts Institute of Technology *
* Yale University *
* *
* http://hstore.cs.brown.edu/ *
* *
* Permission is hereby granted, free of charge, to any person obtaining *
* a copy of this software and associated documentation files (the *
* "Software"), to deal in the Software without restriction, including *
* without limitation the rights to use, copy, modify, merge, publish, *
* distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to *
* the following conditions: *
* *
* The above copyright notice and this permission notice shall be *
* included in all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, *
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF *
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. *
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR *
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
* OTHER DEALINGS IN THE SOFTWARE. *
***************************************************************************/
package edu.brown.benchmark.seats;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import org.apache.commons.collections15.map.ListOrderedMap;
import org.apache.commons.collections15.set.ListOrderedSet;
import org.apache.log4j.Logger;
import org.voltdb.CatalogContext;
import org.voltdb.VoltTable;
import org.voltdb.VoltType;
import org.voltdb.catalog.CatalogMap;
import org.voltdb.catalog.Column;
import org.voltdb.catalog.Table;
import org.voltdb.types.TimestampType;
import org.voltdb.utils.Pair;
import edu.brown.api.Loader;
import edu.brown.benchmark.seats.util.DistanceUtil;
import edu.brown.benchmark.seats.util.Reservation;
import edu.brown.benchmark.seats.util.ReturnFlight;
import edu.brown.benchmark.seats.util.SEATSHistogramUtil;
import edu.brown.catalog.CatalogUtil;
import edu.brown.logging.LoggerUtil;
import edu.brown.logging.LoggerUtil.LoggerBoolean;
import edu.brown.rand.AbstractRandomGenerator;
import edu.brown.rand.DefaultRandomGenerator;
import edu.brown.rand.RandomDistribution;
import edu.brown.rand.RandomDistribution.Flat;
import edu.brown.rand.RandomDistribution.FlatHistogram;
import edu.brown.rand.RandomDistribution.Gaussian;
import edu.brown.rand.RandomDistribution.Zipf;
import edu.brown.statistics.Histogram;
import edu.brown.statistics.ObjectHistogram;
import edu.brown.utils.CollectionUtil;
import edu.brown.utils.StringUtil;
import edu.brown.utils.TableDataIterable;
public class SEATSLoader extends Loader {
private static final Logger LOG = Logger.getLogger(SEATSLoader.class);
private static final LoggerBoolean debug = new LoggerBoolean();
private static final LoggerBoolean trace = new LoggerBoolean();
static {
LoggerUtil.attachObserver(LOG, debug, trace);
}
// -----------------------------------------------------------------
// INTERNAL DATA MEMBERS
// -----------------------------------------------------------------
protected final SEATSProfile profile;
protected final File airline_data_dir;
/**
* Mapping from Airports to their geolocation coordinates
* AirportCode -> <Latitude, Longitude>
*/
private final ListOrderedMap<String, Pair<Double, Double>> airport_locations = new ListOrderedMap<String, Pair<Double,Double>>();
/**
* AirportCode -> Set<AirportCode, Distance>
* Only store the records for those airports in HISTOGRAM_FLIGHTS_PER_AIRPORT
*/
private final Map<String, Map<String, Short>> airport_distances = new HashMap<String, Map<String, Short>>();
/**
* Counter for the number of tables that we have finished loading
*/
private final AtomicInteger finished = new AtomicInteger(0);
private final int num_data_tables;
/**
* A histogram of the number of flights in the database per airline code
*/
private final Histogram<String> flights_per_airline = new ObjectHistogram<String>(true);
private final AbstractRandomGenerator rng;
/**
*
*/
private final Map<Long, FlightInfo> flight_infos = new HashMap<Long, FlightInfo>();
private static class FlightInfo {
// NOTE: These need to be strings.
// We will automagically convert them to their proper
// ids in loadTable()
private String depart_airport;
private String arrive_airport;
private String airline_code;
private TimestampType depart_time;
private TimestampType arrive_time;
private int seats_remaining = SEATSConstants.FLIGHTS_NUM_SEATS;
/**
* Decrement the number of available seats for a flight and return
* the total amount remaining
*/
public int decrementFlightSeat() {
assert(this.seats_remaining > 0) : "Invalid seat count";
return (--this.seats_remaining);
}
}
// -----------------------------------------------------------------
// INITIALIZATION
// -----------------------------------------------------------------
/**
*
* @param args
* @throws Exception
*/
public static void main(String args[]) throws Exception {
edu.brown.api.BenchmarkComponent.main(SEATSLoader.class, args, true);
}
/**
* Constructor
* @param args
*/
public SEATSLoader(String[] args) {
super(args);
String data_dir = null;
for (String key : m_extraParams.keySet()) {
String value = m_extraParams.get(key);
// Data Directory
// Parameter that points to where we can find the initial data files
if (key.equalsIgnoreCase("datadir")) {
data_dir = value;
}
} // FOR
if (data_dir == null) {
throw new RuntimeException("Unable to start benchmark. Missing 'datadir' parameter\n" + StringUtil.formatMaps(m_extraParams));
}
this.airline_data_dir = new File(data_dir);
if (this.airline_data_dir.exists() == false) {
String msg = String.format("Unable to start benchmark. The data directory %s does not exist",
this.airline_data_dir.getAbsolutePath());
throw new RuntimeException(msg);
}
this.rng = new DefaultRandomGenerator();
this.profile = new SEATSProfile(this.getCatalogContext(), this.rng);
this.profile.scale_factor = this.getScaleFactor();
CatalogContext catalogContext = this.getCatalogContext();
int table_ctr = 0;
for (Table catalog_tbl : catalogContext.getDataTables()) {
if (catalog_tbl.getName().startsWith("CONFIG_") == false) {
table_ctr++;
}
} // FOR
this.num_data_tables = table_ctr;
}
public SEATSProfile getProfile() {
return (this.profile);
}
// -----------------------------------------------------------------
// LOADING METHODS
// -----------------------------------------------------------------
@Override
public void load() throws IOException {
if (debug.val) LOG.debug("Begin to load tables...");
CatalogContext catalogContext = this.getCatalogContext();
// Load Histograms
if (debug.val) LOG.debug("Loading data files for histograms");
this.loadHistograms(catalogContext);
// Load the first tables from data files
if (debug.val) LOG.debug("Loading data files for fixed-sized tables");
this.loadFixedTables(catalogContext);
// Once we have those mofos, let's go get make our flight data tables
if (debug.val) LOG.debug("Loading data files for scaling tables");
this.loadScalingTables(catalogContext);
// Save the benchmark profile out to disk so that we can send it
// to all of the clients
this.profile.saveProfile(this);
if (debug.val) LOG.debug("SEATS loader done.");
}
/**
* Load all the histograms used in the benchmark
*/
protected void loadHistograms(CatalogContext catalogContext) {
if (debug.val)
LOG.debug(String.format("Loading in %d histograms from files stored in '%s'",
SEATSConstants.HISTOGRAM_DATA_FILES.length, this.airline_data_dir));
// Now load in the histograms that we will need for generating the flight data
for (String histogramName : SEATSConstants.HISTOGRAM_DATA_FILES) {
if (this.profile.histograms.containsKey(histogramName)) {
if (debug.val)
LOG.warn("Already loaded histogram '" + histogramName + "'. Skipping...");
continue;
}
if (debug.val)
LOG.debug("Loading in histogram data file for '" + histogramName + "'");
Histogram<String> hist = null;
try {
// The Flights_Per_Airport histogram is actually a serialized map that has a histogram
// of the departing flights from each airport to all the others
if (histogramName.equals(SEATSConstants.HISTOGRAM_FLIGHTS_PER_AIRPORT)) {
Map<String, Histogram<String>> m = SEATSHistogramUtil.loadAirportFlights(this.airline_data_dir);
assert(m != null);
if (debug.val)
LOG.debug(String.format("Loaded %d airport flight histograms", m.size()));
// Store the airport codes information
this.profile.airport_histograms.putAll(m);
// We then need to flatten all of the histograms in this map into a single histogram
// that just counts the number of departing flights per airport. We will use this
// to get the distribution of where Customers are located
hist = new ObjectHistogram<String>();
for (Entry<String, Histogram<String>> e : m.entrySet()) {
hist.put(e.getKey(), e.getValue().getSampleCount());
} // FOR
// All other histograms are just serialized and can be loaded directly
} else {
hist = SEATSHistogramUtil.loadHistogram(histogramName, this.airline_data_dir, true);
}
} catch (Exception ex) {
throw new RuntimeException("Failed to load histogram '" + histogramName + "'", ex);
}
assert(hist != null);
this.profile.histograms.put(histogramName, hist);
if (debug.val) LOG.debug(String.format("Loaded histogram '%s' [sampleCount=%d, valueCount=%d]",
histogramName, hist.getSampleCount(), hist.getValueCount()));
} // FOR
}
/**
* The fixed tables are those that are generated from the static data files
* The number of tuples in these tables will not change based on the scale factor.
* @param catalogContext
*/
protected void loadFixedTables(CatalogContext catalogContext) {
for (String table_name : SEATSConstants.TABLES_DATAFILES) {
LOG.debug(String.format("Loading table '%s' from fixed file", table_name));
try {
Table catalog_tbl = catalogContext.database.getTables().getIgnoreCase(table_name);
assert(catalog_tbl != null);
Iterable<Object[]> iterable = this.getFixedIterable(catalog_tbl);
this.loadTable(catalog_tbl, iterable, 5000);
} catch (Throwable ex) {
throw new RuntimeException("Failed to load data files for fixed-sized table '" + table_name + "'", ex);
}
} // FOR
}
/**
* The scaling tables are things that we will scale the number of tuples based
* on the given scaling factor at runtime
* @param catalogContext
*/
protected void loadScalingTables(CatalogContext catalogContext) {
// Setup the # of flights per airline
this.flights_per_airline.put(profile.getAirlineCodes(), 0);
// IMPORTANT: FLIGHT must come before FREQUENT_FLYER so that we
// can use the flights_per_airline histogram when selecting an airline to
// create a new FREQUENT_FLYER account for a CUSTOMER
for (String table_name : SEATSConstants.TABLES_SCALING) {
try {
Table catalog_tbl = catalogContext.database.getTables().get(table_name);
assert(catalog_tbl != null);
Iterable<Object[]> iterable = this.getScalingIterable(catalog_tbl);
this.loadTable(catalog_tbl, iterable, 5000);
} catch (Throwable ex) {
ex.printStackTrace();
String msg = "Failed to load data files for scaling-sized table '" + table_name + "'";
throw new RuntimeException(msg, ex);
}
} // FOR
}
/**
* Debug
* @param catalog_tbl
* @param tuple
* @param row_idx
* @return
*/
private String dumpTuple(Table catalog_tbl, Object tuple[], int row_idx) {
String ret = catalog_tbl.getName() + " #" + row_idx + "\n";
for (int i = 0; i < tuple.length; i++) {
Column catalog_col = catalog_tbl.getColumns().get(i);
ret += "[" + i + "]: " + tuple[i] + " - " + catalog_col.getName() + " - [" +
(tuple[i] != null ? tuple[i].getClass().getSimpleName() : null) +
"->" + VoltType.get((byte)catalog_col.getType()).toString() + "]\n";
} // FOR
ret += "\n";
return (ret);
}
/**
*
* @param catalog_tbl
*/
public void loadTable(Table catalog_tbl, Iterable<Object[]> iterable, int batch_size) {
final String tableName = catalog_tbl.getName();
final boolean is_airport = tableName.equals(SEATSConstants.TABLENAME_AIRPORT);
final boolean is_flight = tableName.equals(SEATSConstants.TABLENAME_FLIGHT);
final boolean is_customer = tableName.equals(SEATSConstants.TABLENAME_CUSTOMER);
final boolean is_reservation = tableName.equals(SEATSConstants.TABLENAME_RESERVATION);
final CatalogContext catalogContext = this.getCatalogContext();
if (debug.val)
LOG.debug(String.format("Generating new records for table %s [batchSize=%d]",
tableName, batch_size));
final VoltTable vt = CatalogUtil.getVoltTable(catalog_tbl);
final CatalogMap<Column> columns = catalog_tbl.getColumns();
VoltTable vtFilghtInfo = null;
int flightInfoOffsets[] = null;
if (is_flight) {
Table flightTbl = catalogContext.database.getTables().get(SEATSConstants.TABLENAME_FLIGHT_INFO);
vtFilghtInfo = CatalogUtil.getVoltTable(flightTbl);
flightInfoOffsets = new int[flightTbl.getColumns().size()];
for (Column flightInfoCol : flightTbl.getColumns()) {
Column flightCol = catalog_tbl.getColumns().getIgnoreCase(flightInfoCol.getName());
assert(flightCol != null) : "Missing " + flightInfoCol.fullName();
flightInfoOffsets[flightInfoCol.getIndex()] = flightCol.getIndex();
} // FOR
}
// Check whether we have any special mappings that we need to maintain
Map<Integer, Integer> code_2_id = new HashMap<Integer, Integer>();
Map<Integer, Map<String, Long>> mapping_columns = new HashMap<Integer, Map<String, Long>>();
for (Column catalog_col : columns) {
String col_name = catalog_col.getName();
int col_code_idx = catalog_col.getIndex();
// Code Column -> Id Column Mapping
// Check to see whether this table has columns that we need to map their
// code values to tuple ids
String col_id_name = this.profile.code_columns.get(col_name);
if (col_id_name != null) {
Column catalog_id_col = columns.get(col_id_name);
assert(catalog_id_col != null) : "The id column " + tableName + "." + col_id_name + " is missing";
int col_id_idx = catalog_id_col.getIndex();
code_2_id.put(col_code_idx, col_id_idx);
}
// Foreign Key Column to Code->Id Mapping
// If this columns references a foreign key that is used in the Code->Id mapping
// that we generating above, then we need to know when we should change the
// column value from a code to the id stored in our lookup table
if (this.profile.fkey_value_xref.containsKey(col_name)) {
String col_fkey_name = this.profile.fkey_value_xref.get(col_name);
mapping_columns.put(col_code_idx, this.profile.code_id_xref.get(col_fkey_name));
}
} // FOR
int row_idx = 0;
try {
for (Object tuple[] : iterable) {
assert(tuple[0] != null) : "The primary key for " + tableName + " is null";
if (trace.val) LOG.trace(this.dumpTuple(catalog_tbl, tuple, row_idx));
// AIRPORT
if (is_airport) {
// Skip any airport that does not have flights
int col_code_idx = catalog_tbl.getColumns().get("AP_CODE").getIndex();
if (profile.hasFlights((String)tuple[col_code_idx]) == false) {
if (trace.val)
LOG.trace(String.format("Skipping AIRPORT '%s' because it does not have any flights", tuple[col_code_idx]));
continue;
}
// Update the row # so that it matches what we're actually loading
int col_id_idx = columns.get("AP_ID").getIndex();
tuple[col_id_idx] = (long)(row_idx + 1);
// Store Locations
int col_lat_idx = catalog_tbl.getColumns().get("AP_LATITUDE").getIndex();
int col_lon_idx = catalog_tbl.getColumns().get("AP_LONGITUDE").getIndex();
Pair<Double, Double> coords = Pair.of((Double)tuple[col_lat_idx], (Double)tuple[col_lon_idx]);
if (coords.getFirst() == null || coords.getSecond() == null) {
LOG.error(Arrays.toString(tuple));
}
assert(coords.getFirst() != null) :
String.format("Unexpected null latitude for airport '%s' [%d]", tuple[col_code_idx], col_lat_idx);
assert(coords.getSecond() != null) :
String.format("Unexpected null longitude for airport '%s' [%d]", tuple[col_code_idx], col_lon_idx);
this.airport_locations.put(tuple[col_code_idx].toString(), coords);
if (trace.val)
LOG.trace(String.format("Storing location for '%s': %s", tuple[col_code_idx], coords));
}
// Code Column -> Id Column
for (int col_code_idx : code_2_id.keySet()) {
assert(tuple[col_code_idx] != null) :
String.format("The value of the code column at '%d' is null for %s\n%s",
col_code_idx, tableName, Arrays.toString(tuple));
String code = tuple[col_code_idx].toString().trim();
if (code.length() > 0) {
Column from_column = columns.get(col_code_idx);
assert(from_column != null);
Column to_column = columns.get(code_2_id.get(col_code_idx));
assert(to_column != null) : String.format("Invalid column %s.%s", tableName, code_2_id.get(col_code_idx));
long id = (Long)tuple[code_2_id.get(col_code_idx)];
if (trace.val) LOG.trace(String.format("Mapping %s '%s' -> %s '%d'", from_column.fullName(), code, to_column.fullName(), id));
this.profile.code_id_xref.get(to_column.getName()).put(code, id);
}
} // FOR
// Foreign Key Code -> Foreign Key Id
for (int col_code_idx : mapping_columns.keySet()) {
Column catalog_col = columns.get(col_code_idx);
assert(tuple[col_code_idx] != null || catalog_col.getNullable()) :
String.format("The code %s column at '%d' is null for %s id=%s\n%s",
catalog_col.fullName(), col_code_idx, tableName, tuple[0], Arrays.toString(tuple));
if (tuple[col_code_idx] != null) {
String code = tuple[col_code_idx].toString();
tuple[col_code_idx] = mapping_columns.get(col_code_idx).get(code);
if (trace.val) {
Column catalog_fkey_col = CatalogUtil.getForeignKeyParent(catalog_col);
LOG.trace(String.format("Mapped %s '%s' -> %s '%s'",
catalog_col.fullName(), code, catalog_fkey_col.fullName(), tuple[col_code_idx]));
}
}
} // FOR
vt.addRow(tuple);
if (is_flight) {
if (debug.val)
LOG.debug(String.format("#1#FlightInfo is adding row...\nVT: %s\nVTFlightINFO:%s",
tuple.toString(), vt.toString(), vtFilghtInfo.toString()));
Object[] partTuple = new Object[vtFilghtInfo.getColumnCount()];
for (int i = 0 ;i < partTuple.length; i++)
partTuple[i] = tuple[flightInfoOffsets[i]];
vtFilghtInfo.addRow(partTuple);
}
if (row_idx > 0 && (row_idx+1) % batch_size == 0) {
// if (trace.val) LOG.trace("Storing batch of " + batch_size + " tuples for " + tableName + " [total=" + row_idx + "]");
// if (debug) System.out.println(vt.toString());
this.loadVoltTable(tableName, vt);
vt.clearRowData();
if (is_flight) {
if (debug.val)
LOG.debug(String.format("#1#FlightInfo is adding row...\nVT: %s\nVTFlightINFO:%s",
tuple.toString(), vt.toString(), vtFilghtInfo.toString()));
this.loadVoltTable(SEATSConstants.TABLENAME_FLIGHT_INFO, vtFilghtInfo);
vtFilghtInfo.clearRowData();
}
}
row_idx++;
} // FOR
} catch (Exception ex) {
throw new RuntimeException("Failed to load table " + tableName, ex);
}
if (vt.getRowCount() > 0) {
this.loadVoltTable(tableName, vt);
if (is_flight)
this.loadVoltTable(SEATSConstants.TABLENAME_FLIGHT_INFO, vtFilghtInfo);
}
if (is_airport) assert(profile.getAirportCount() == row_idx) :
String.format("%d != %d", profile.getAirportCount(), row_idx);
// Record the number of tuples that we loaded for various tables in the profile
if (is_flight) {
this.profile.num_flights = row_idx;
}
else if (is_reservation) {
this.profile.num_reservations = row_idx;
}
else if (is_customer) {
this.profile.num_customers = row_idx;
}
LOG.info(String.format("Finished loading all %d tuples for %s [%d / %d]",
row_idx, tableName,
this.finished.incrementAndGet(), this.num_data_tables));
if (is_flight) {
LOG.info(String.format("Finished loading all %d tuples for %s [%d / %d]",
row_idx, SEATSConstants.TABLENAME_FLIGHT_INFO,
this.finished.incrementAndGet(), this.num_data_tables));
}
return;
}
// ----------------------------------------------------------------
// FIXED-SIZE TABLE DATA GENERATION
// ----------------------------------------------------------------
/**
*
* @param catalog_tbl
* @return
* @throws Exception
*/
protected Iterable<Object[]> getFixedIterable(Table catalog_tbl) throws Exception {
File f = SEATSProjectBuilder.getTableDataFile(this.airline_data_dir, catalog_tbl);
TableDataIterable iterable = new FixedDataIterable(catalog_tbl, f);
return (iterable);
}
/**
* Wrapper around TableDataIterable that will populate additional random fields
*/
protected class FixedDataIterable extends TableDataIterable {
private final Set<Integer> rnd_string = new HashSet<Integer>();
private final Map<Integer, Integer> rnd_string_min = new HashMap<Integer, Integer>();
private final Map<Integer, Integer> rnd_string_max = new HashMap<Integer, Integer>();
private final Set<Integer> rnd_integer = new HashSet<Integer>();
public FixedDataIterable(Table catalog_tbl, File filename) throws Exception {
super(catalog_tbl, filename, true, true);
// Figure out which columns are random integers and strings
for (Column catalog_col : catalog_tbl.getColumns()) {
String col_name = catalog_col.getName();
int col_idx = catalog_col.getIndex();
if (col_name.contains("_SATTR")) {
this.rnd_string.add(col_idx);
this.rnd_string_min.put(col_idx, rng.nextInt(catalog_col.getSize()-1));
this.rnd_string_max.put(col_idx, catalog_col.getSize());
} else if (col_name.contains("_IATTR")) {
this.rnd_integer.add(catalog_col.getIndex());
}
} // FOR
}
@Override
public Iterator<Object[]> iterator() {
// This is nasty old boy!
return (new TableDataIterable.TableIterator() {
@Override
public Object[] next() {
Object[] tuple = super.next();
// Random String (*_SATTR##)
for (int col_idx : rnd_string) {
int min_length = rnd_string_min.get(col_idx);
int max_length = rnd_string_max.get(col_idx);
tuple[col_idx] = rng.astring(min_length, max_length);
} // FOR
// Random Integer (*_IATTR##)
for (int col_idx : rnd_integer) {
tuple[col_idx] = rng.nextLong();
} // FOR
return (tuple);
}
});
}
} // END CLASS
// ----------------------------------------------------------------
// SCALING TABLE DATA GENERATION
// ----------------------------------------------------------------
/**
* Return an iterable that spits out tuples for scaling tables
* @param catalog_tbl the target table that we need an iterable for
*/
protected Iterable<Object[]> getScalingIterable(Table catalog_tbl) {
String name = catalog_tbl.getName();
ScalingDataIterable it = null;
double scaleFactor = this.getScaleFactor();
long num_customers = Math.round(SEATSConstants.CUSTOMERS_COUNT * scaleFactor);
// Customers
if (name.equalsIgnoreCase(SEATSConstants.TABLENAME_CUSTOMER)) {
it = new CustomerIterable(catalog_tbl, num_customers);
}
// FrequentFlyer
else if (name.equalsIgnoreCase(SEATSConstants.TABLENAME_FREQUENT_FLYER)) {
it = new FrequentFlyerIterable(catalog_tbl, num_customers);
}
// Airport Distance
else if (name.equalsIgnoreCase(SEATSConstants.TABLENAME_AIRPORT_DISTANCE)) {
int max_distance = Integer.MAX_VALUE; // SEATSConstants.DISTANCES[SEATSConstants.DISTANCES.length - 1];
it = new AirportDistanceIterable(catalog_tbl, max_distance);
}
// Flights
else if (name.equalsIgnoreCase(SEATSConstants.TABLENAME_FLIGHT)) {
it = new FlightIterable(catalog_tbl,
(int)Math.round(SEATSConstants.FLIGHTS_DAYS_PAST * scaleFactor),
(int)Math.round(SEATSConstants.FLIGHTS_DAYS_FUTURE * scaleFactor));
}
// Reservations
else if (name.equalsIgnoreCase(SEATSConstants.TABLENAME_RESERVATION)) {
long total = Math.round((SEATSConstants.FLIGHTS_PER_DAY_MIN + SEATSConstants.FLIGHTS_PER_DAY_MAX) / 2d * scaleFactor);
it = new ReservationIterable(catalog_tbl, total);
}
else {
assert(false) : "Unexpected table '" + name + "' in getScalingIterable()";
}
assert(it != null) : "The ScalingIterable for '" + name + "' is null!";
return (it);
}
/**
* Base Iterable implementation for scaling tables
* Sub-classes implement the specialValue() method to generate values of a specific
* type instead of just using the random data generators
*/
protected abstract class ScalingDataIterable implements Iterable<Object[]> {
private final Table catalog_tbl;
private final boolean special[];
private final Object[] data;
private final VoltType types[];
/**
* The total number of rows that this iterable will create
*/
protected long total;
private long last_id = 0;
/**
* Constructor
* @param catalog_tbl
* @param table_file
* @throws Exception
*/
public ScalingDataIterable(Table catalog_tbl, long total) {
this(catalog_tbl, total, new int[0]);
}
/**
*
* @param catalog_tbl
* @param total
* @param special_columns - The offsets of the columns that we will invoke specialValue() to get their values
* @throws Exception
*/
public ScalingDataIterable(Table catalog_tbl, long total, int special_columns[]) {
this.catalog_tbl = catalog_tbl;
this.total = total;
this.data = new Object[this.catalog_tbl.getColumns().size()];
this.special = new boolean[this.catalog_tbl.getColumns().size()];
for (int i = 0; i < this.special.length; i++) {
this.special[i] = false;
}
for (int idx : special_columns) {
this.special[idx] = true;
}
// Cache the types
this.types = new VoltType[catalog_tbl.getColumns().size()];
for (Column catalog_col : catalog_tbl.getColumns()) {
this.types[catalog_col.getIndex()] = VoltType.get(catalog_col.getType());
} // FOR
}
/**
* Generate a special value for this particular column index
* @param idx
* @return
*/
protected abstract Object specialValue(long id, int column_idx);
/**
* Simple callback when the ScalingDataIterable is finished
*/
protected void callbackFinished() {
// Nothing...
}
protected boolean hasNext() {
boolean has_next = (last_id < total);
if (has_next == false) this.callbackFinished();
return (has_next);
}
/**
* Generate the iterator
*/
public Iterator<Object[]> iterator() {
Iterator<Object[]> it = new Iterator<Object[]>() {
@Override
public boolean hasNext() {
return (ScalingDataIterable.this.hasNext());
}
@Override
public Object[] next() {
for (int i = 0; i < data.length; i++) {
Column catalog_col = catalog_tbl.getColumns().get(i);
assert(catalog_col != null) : "The column at position " + i + " for " + catalog_tbl + " is null";
// Special Value Column
if (special[i]) {
data[i] = specialValue(last_id, i);
// Id column (always first unless overridden in special)
} else if (i == 0) {
data[i] = new Long(last_id);
// Strings
} else if (types[i] == VoltType.STRING) {
int size = catalog_col.getSize();
data[i] = rng.astring(rng.nextInt(size - 1), size);
// Timestamps
} else if (types[i] == VoltType.TIMESTAMP) {
data[i] = new TimestampType();
// Ints/Longs
} else {
data[i] = rng.number(0, 1<<30);
}
} // FOR
last_id++;
return (data);
}
@Override
public void remove() {
// Not Implemented
}
};
return (it);
}
} // END CLASS
// ----------------------------------------------------------------
// CUSTOMERS
// ----------------------------------------------------------------
protected class CustomerIterable extends ScalingDataIterable {
private long NEXT_ID = 0;
private final FlatHistogram<String> rand;
private final RandomDistribution.Flat randBalance;
private String airport_code = null;
private Long last_id = null;
public CustomerIterable(Table catalog_tbl, long total) {
super(catalog_tbl, total, new int[]{ 0, 1, 2, 3 });
// Use the flights per airport histogram to select where people are located
Histogram<String> histogram = profile.getHistogram(SEATSConstants.HISTOGRAM_FLIGHTS_PER_AIRPORT);
this.rand = new FlatHistogram<String>(rng, histogram);
if (debug.val) this.rand.enableHistory();
this.randBalance = new RandomDistribution.Flat(rng, 1000, 10000);
}
@Override
protected Object specialValue(long id, int columnIdx) {
Object value = null;
switch (columnIdx) {
// CUSTOMER ID
case (0): {
// HACK: The flights_per_airport histogram may not match up exactly with the airport
// data files, so we'll just spin until we get a good one
Long airport_id = null;
while (airport_id == null) {
this.airport_code = this.rand.nextValue();
airport_id = profile.getAirportId(this.airport_code);
} // WHILE
profile.incrementAirportCustomerCount(airport_id);
value = this.last_id = NEXT_ID++;
if (trace.val)
LOG.trace(String.format("%s => %s [%d]",
value, this.airport_code, profile.getCustomerIdCount(airport_id)));
break;
}
// CUSTOMER ID STR
case (1): {
assert(this.last_id != null);
value = String.format(SEATSConstants.CUSTOMER_ID_STR, this.last_id);
this.last_id = null;
break;
}
// LOCAL AIRPORT
case (2): {
assert(this.airport_code != null);
value = this.airport_code;
break;
}
// BALANCE
case (3): {
value = (double)this.randBalance.nextInt();
break;
}
// BAD MOJO!
default:
assert(false) : "Unexpected special column index " + columnIdx;
} // SWITCH
return (value);
}
@Override
protected void callbackFinished() {
if (trace.val) {
Histogram<String> h = this.rand.getHistogramHistory();
LOG.trace(String.format("Customer Local Airports Histogram [valueCount=%d, sampleCount=%d]\n%s",
h.getValueCount(), h.getSampleCount(), h.toString()));
}
}
}
// ----------------------------------------------------------------
// FREQUENT_FLYER
// ----------------------------------------------------------------
protected class FrequentFlyerIterable extends ScalingDataIterable {
private final short ff_per_customer[];
private final FlatHistogram<String> airline_rand;
private int last_customer_id = 0;
private Collection<String> customer_airlines = new HashSet<String>();
public FrequentFlyerIterable(Table catalog_tbl, long num_customers) {
super(catalog_tbl, num_customers, new int[]{ 0, 1, 2 });
// A customer is more likely to have a FREQUENTY_FLYER account with
// an airline that has more flights.
// IMPORTANT: Add one to all of the airlines so that we don't get trapped
// in an infinite loop
assert(flights_per_airline.isEmpty() == false);
flights_per_airline.putAll();
this.airline_rand = new FlatHistogram<String>(rng, flights_per_airline);
if (trace.val) this.airline_rand.enableHistory();
if (debug.val) LOG.debug("Flights Per Airline:\n" + flights_per_airline);
// Loop through for the total customers and figure out how many entries we
// should have for each one. This will be our new total;
long max_per_customer = Math.min(Math.round(SEATSConstants.CUSTOMER_NUM_FREQUENTFLYERS_MAX * Math.max(1, getScaleFactor())),
flights_per_airline.getValueCount());
Zipf ff_zipf = new Zipf(rng, SEATSConstants.CUSTOMER_NUM_FREQUENTFLYERS_MIN,
max_per_customer,
SEATSConstants.CUSTOMER_NUM_FREQUENTFLYERS_SIGMA);
long new_total = 0;
if (debug.val) LOG.debug("Num of Customers: " + num_customers);
this.ff_per_customer = new short[(int)num_customers];
for (int i = 0; i < num_customers; i++) {
this.ff_per_customer[i] = (short)ff_zipf.nextInt();
if (this.ff_per_customer[i] > max_per_customer)
this.ff_per_customer[i] = (short)max_per_customer;
new_total += this.ff_per_customer[i];
} // FOR
this.total = new_total;
if (debug.val) LOG.debug("Constructing " + this.total + " FrequentFlyer tuples...");
}
@Override
protected Object specialValue(long id, int columnIdx) {
Object value = null;
switch (columnIdx) {
// CUSTOMER ID
case (0): {
while (this.last_customer_id < this.ff_per_customer.length &&
this.ff_per_customer[this.last_customer_id] <= 0) {
this.last_customer_id++;
this.customer_airlines.clear();
if (trace.val)
LOG.trace(String.format("NEXT CUSTOMER: %d / %d",
this.last_customer_id, profile.getCustomerIdCount()));
} // WHILE
this.ff_per_customer[this.last_customer_id]--;
value = this.last_customer_id;
break;
}
// AIRLINE ID
case (1): {
assert(this.customer_airlines.size() < flights_per_airline.getValueCount());
do {
value = this.airline_rand.nextValue();
} while (this.customer_airlines.contains(value));
this.customer_airlines.add((String)value);
if (trace.val) LOG.trace(this.last_customer_id + " => " + value);
break;
}
// CUSTOMER_ID_STR
case (2): {
value = String.format(SEATSConstants.CUSTOMER_ID_STR, this.last_customer_id);
break;
}
// BAD MOJO!
default:
assert(false) : "Unexpected special column index " + columnIdx;
} // SWITCH
return (value);
}
@Override
protected void callbackFinished() {
if (trace.val) {
Histogram<String> h = this.airline_rand.getHistogramHistory();
LOG.trace(String.format("Airline Flights Histogram [valueCount=%d, sampleCount=%d]\n%s",
h.getValueCount(), h.getSampleCount(), h.toString()));
}
}
}
// ----------------------------------------------------------------
// AIRPORT_DISTANCE
// ----------------------------------------------------------------
protected class AirportDistanceIterable extends ScalingDataIterable {
private final int max_distance;
private final int num_airports;
private final Collection<String> record_airports;
private int outer_ctr = 0;
private String outer_airport;
private Pair<Double, Double> outer_location;
private Integer last_inner_ctr = null;
private String inner_airport;
private Pair<Double, Double> inner_location;
private double distance;
/**
* Constructor
* @param catalog_tbl
* @param max_distance
*/
public AirportDistanceIterable(Table catalog_tbl, int max_distance) {
super(catalog_tbl, Long.MAX_VALUE, new int[] { 0, 1, 2 }); // total work around ????
this.max_distance = max_distance;
this.num_airports = airport_locations.size();
this.record_airports = profile.getAirportCodes();
}
/**
* Find the next two airports that are within our max_distance limit
* We keep track of where we were in the inner loop using last_inner_ctr
*/
@Override
protected boolean hasNext() {
for ( ; this.outer_ctr < (this.num_airports - 1); this.outer_ctr++) {
this.outer_airport = airport_locations.get(this.outer_ctr);
this.outer_location = airport_locations.getValue(this.outer_ctr);
if (profile.hasFlights(this.outer_airport) == false) continue;
int inner_ctr = (this.last_inner_ctr != null ? this.last_inner_ctr : this.outer_ctr + 1);
this.last_inner_ctr = null;
for ( ; inner_ctr < this.num_airports; inner_ctr++) {
assert(this.outer_ctr != inner_ctr);
this.inner_airport = airport_locations.get(inner_ctr);
this.inner_location = airport_locations.getValue(inner_ctr);
if (profile.hasFlights(this.inner_airport) == false) continue;
this.distance = DistanceUtil.distance(this.outer_location, this.inner_location);
// Store the distance between the airports locally if either one is in our
// flights-per-airport data set
if (this.record_airports.contains(this.outer_airport) &&
this.record_airports.contains(this.inner_airport)) {
SEATSLoader.this.setDistance(this.outer_airport, this.inner_airport, this.distance);
}
// Stop here if these two airports are within range
if (this.distance > 0 && this.distance <= this.max_distance) {
// System.err.println(this.outer_airport + "->" + this.inner_airport + ": " + distance);
this.last_inner_ctr = inner_ctr + 1;
return (true);
}
} // FOR
} // FOR
return (false);
}
@Override
protected Object specialValue(long id, int columnIdx) {
Object value = null;
switch (columnIdx) {
// OUTER AIRPORT
case (0):
value = this.outer_airport;
break;
// INNER AIRPORT
case (1):
value = this.inner_airport;
break;
// DISTANCE
case (2):
value = this.distance;
break;
// BAD MOJO!
default:
assert(false) : "Unexpected special column index " + columnIdx;
} // SWITCH
return (value);
}
}
// ----------------------------------------------------------------
// FLIGHTS
// ----------------------------------------------------------------
protected class FlightIterable extends ScalingDataIterable {
private long NEXT_FLIGHT_ID = 0;
private final FlatHistogram<String> airlines;
private final FlatHistogram<String> airports;
private final Map<String, FlatHistogram<String>> flights_per_airport = new HashMap<String, FlatHistogram<String>>();
private final FlatHistogram<String> flight_times;
private final Flat prices;
// private final Set<FlightId> todays_flights = new HashSet<FlightId>();
private final Set<Long> todays_flights = new HashSet<Long>();
private final ListOrderedMap<TimestampType, Integer> flights_per_day = new ListOrderedMap<TimestampType, Integer>();
private int day_idx = 0;
private TimestampType today;
private TimestampType start_date;
// private FlightId flight_id;
private long flight_id;
private FlightInfo flightInfo;
public FlightIterable(Table catalog_tbl, int days_past, int days_future) {
super(catalog_tbl, Long.MAX_VALUE, new int[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8 });
assert(days_past >= 0);
assert(days_future >= 0);
this.prices = new Flat(rng,
SEATSConstants.RESERVATION_PRICE_MIN,
SEATSConstants.RESERVATION_PRICE_MAX);
// Flights per Airline
Collection<String> all_airlines = profile.getAirlineCodes();
Histogram<String> histogram = new ObjectHistogram<String>();
histogram.put(all_airlines);
// Embed a Gaussian distribution
Gaussian gauss_rng = new Gaussian(rng, 0, all_airlines.size());
this.airlines = new FlatHistogram<String>(gauss_rng, histogram);
// Flights Per Airport
histogram = profile.getHistogram(SEATSConstants.HISTOGRAM_FLIGHTS_PER_AIRPORT);
this.airports = new FlatHistogram<String>(rng, histogram);
for (String airport_code : histogram.values()) {
histogram = profile.getFightsPerAirportHistogram(airport_code);
assert(histogram != null) : "Unexpected departure airport code '" + airport_code + "'";
this.flights_per_airport.put(airport_code, new FlatHistogram<String>(rng, histogram));
} // FOR
// Flights Per Departure Time
histogram = profile.getHistogram(SEATSConstants.HISTOGRAM_FLIGHTS_PER_DEPART_TIMES);
this.flight_times = new FlatHistogram<String>(rng, histogram);
// Figure out how many flights that we want for each day
this.today = new TimestampType();
// Sometimes there are more flights per day, and sometimes there are fewer
int flightsPerDayMin = (int)Math.round(SEATSConstants.FLIGHTS_PER_DAY_MIN * getScaleFactor());
int flightsPerDayMax = (int)Math.round(SEATSConstants.FLIGHTS_PER_DAY_MAX * getScaleFactor());
Gaussian gaussian = new Gaussian(rng, flightsPerDayMin, flightsPerDayMax);
this.total = 0;
boolean first = true;
for (long t = this.today.getTime() - (days_past * SEATSConstants.MICROSECONDS_PER_DAY);
t < this.today.getTime(); t += SEATSConstants.MICROSECONDS_PER_DAY) {
TimestampType timestamp = new TimestampType(t);
if (first) {
this.start_date = timestamp;
first = false;
}
int num_flights = gaussian.nextInt();
this.flights_per_day.put(timestamp, num_flights);
this.total += num_flights;
} // FOR
if (this.start_date == null) this.start_date = this.today;
profile.setFlightStartDate(this.start_date);
// This is for upcoming flights that we want to be able to schedule
// new reservations for in the benchmark
profile.setFlightUpcomingDate(this.today);
for (long t = this.today.getTime(), last_date = this.today.getTime() + (days_future * SEATSConstants.MICROSECONDS_PER_DAY);
t <= last_date; t += SEATSConstants.MICROSECONDS_PER_DAY) {
TimestampType timestamp = new TimestampType(t);
int num_flights = gaussian.nextInt();
this.flights_per_day.put(timestamp, num_flights);
this.total += num_flights;
} // FOR
// Update profile
profile.setFlightPastDays(days_past);
profile.setFlightFutureDays(days_future);
}
/**
* Convert a time string "HH:MM" to a TimestampType object
* @param code
* @return
*/
private TimestampType convertTimeString(TimestampType base_date, String code) {
Matcher m = SEATSConstants.TIMECODE_PATTERN.matcher(code);
boolean result = m.find();
assert(result) : "Invalid time code '" + code + "'";
int hour = -1;
try {
hour = Integer.valueOf(m.group(1));
} catch (Throwable ex) {
throw new RuntimeException("Invalid HOUR in time code '" + code + "'", ex);
}
assert(hour != -1);
int minute = -1;
try {
minute = Integer.valueOf(m.group(2));
} catch (Throwable ex) {
throw new RuntimeException("Invalid MINUTE in time code '" + code + "'", ex);
}
assert(minute != -1);
long offset = (hour * 60 * SEATSConstants.MICROSECONDS_PER_MINUTE) + (minute * SEATSConstants.MICROSECONDS_PER_MINUTE);
return (new TimestampType(base_date.getTime() + offset));
}
/**
* Select all the data elements for the current tuple
* @param date
*/
private FlightInfo populate(long flight_id, TimestampType date) {
FlightInfo flightInfo = new FlightInfo();
// Depart/Arrive Airports
String airport_code = this.airports.nextValue();
flightInfo.depart_airport = airport_code;
flightInfo.arrive_airport = this.flights_per_airport.get(airport_code).nextValue();
if (trace.val)
LOG.trace(String.format("DEPART:%d / ARRIVE:%d",
flightInfo.depart_airport, flightInfo.arrive_airport));
// Depart/Arrive Times
flightInfo.depart_time = this.convertTimeString(date, this.flight_times.nextValue());
flightInfo.arrive_time = SEATSLoader.this.calculateArrivalTime(flightInfo.depart_airport,
flightInfo.arrive_airport,
flightInfo.depart_time);
// Airline
flightInfo.airline_code = this.airlines.nextValue();
// flightInfo.airline_id = profile.getAirlineId(flightInfo.airline_code);
this.flights_per_day.put(date, this.flights_per_day.get(date) - 1);
SEATSLoader.this.flight_infos.put(flight_id, flightInfo);
return (flightInfo);
}
/**
* Returns true if this seat is occupied (which means we must generate a reservation)
*/
boolean seatIsOccupied() {
return (rng.nextInt(100) < SEATSConstants.PROB_SEAT_OCCUPIED);
}
@Override
protected Object specialValue(long id, int columnIdx) {
Object value = null;
switch (columnIdx) {
// FLIGHT ID
case (0): {
// Figure out what date we are currently on
Integer remaining = null;
TimestampType date;
do {
// Move to the next day.
// Make sure that we reset the set of FlightIds that we've used for today
if (remaining != null) {
this.todays_flights.clear();
this.day_idx++;
}
date = this.flights_per_day.get(this.day_idx);
remaining = this.flights_per_day.getValue(this.day_idx);
} while (remaining <= 0 && this.day_idx + 1 < this.flights_per_day.size());
assert(date != null);
value = this.flight_id = NEXT_FLIGHT_ID++;
this.flightInfo = this.populate(this.flight_id, date);
this.todays_flights.add(this.flight_id);
break;
}
// AIRLINE ID
case (1): {
value = this.flightInfo.airline_code;
flights_per_airline.put(this.flightInfo.airline_code);
break;
}
// DEPART AIRPORT
case (2): {
value = this.flightInfo.depart_airport;
if (trace.val)
LOG.trace("Flight=" + this.flight_id + " / DEPART:" + this.flightInfo.depart_airport);
break;
}
// DEPART TIME
case (3): {
value = this.flightInfo.depart_time;
break;
}
// ARRIVE AIRPORT
case (4): {
value = this.flightInfo.arrive_airport;
if (trace.val)
LOG.trace("Flight=" + this.flight_id + " / ARRIVE:" + this.flightInfo.arrive_airport);
break;
}
// ARRIVE TIME
case (5): {
value = this.flightInfo.arrive_time;
break;
}
// BASE PRICE
case (6): {
value = (double)this.prices.nextInt();
break;
}
// SEATS TOTAL
case (7): {
value = SEATSConstants.FLIGHTS_NUM_SEATS;
break;
}
// SEATS REMAINING
case (8): {
// We have to figure this out ahead of time since we need to populate the tuple now
for (int seatnum = 0; seatnum < SEATSConstants.FLIGHTS_NUM_SEATS; seatnum++) {
if (!this.seatIsOccupied()) continue;
this.flightInfo.decrementFlightSeat();
} // FOR
value = this.flightInfo.seats_remaining;
if (trace.val) LOG.trace(this.flight_id + " SEATS REMAINING: " + value);
break;
}
// BAD MOJO!
default:
assert(false) : "Unexpected special column index " + columnIdx;
} // SWITCH
return (value);
}
}
// ----------------------------------------------------------------
// RESERVATIONS
// ----------------------------------------------------------------
protected class ReservationIterable extends ScalingDataIterable {
private final RandomDistribution.Flat prices = new RandomDistribution.Flat(rng, SEATSConstants.RESERVATION_PRICE_MIN, SEATSConstants.RESERVATION_PRICE_MAX);
/**
* For each airport id, store a list of ReturnFlight objects that represent customers
* that need return flights back to their home airport
* ArriveAirportId -> ReturnFlights
*/
private final Map<Long, TreeSet<ReturnFlight>> airport_returns = new HashMap<Long, TreeSet<ReturnFlight>>();
/**
* When this flag is true, then the data generation thread is finished
*/
private boolean done = false;
/**
* We use a Gaussian distribution for determining how long a customer will stay at their
* destination before needing to return to their original airport
*/
private final Gaussian rand_returns = new Gaussian(rng, SEATSConstants.CUSTOMER_RETURN_FLIGHT_DAYS_MIN,
SEATSConstants.CUSTOMER_RETURN_FLIGHT_DAYS_MAX);
private final LinkedBlockingDeque<Reservation> queue = new LinkedBlockingDeque<Reservation>(100);
private Reservation current = null;
private Throwable error = null;
/**
* Constructor
* @param catalog_tbl
* @param total
*/
public ReservationIterable(Table catalog_tbl, long total) {
// Special Columns: R_C_ID, R_F_ID, R_F_AL_ID, R_SEAT, R_PRICE
super(catalog_tbl, total, new int[] { 1, 2, 3, 4 });
for (long airport_id : profile.getAirportIds()) {
// Return Flights per airport
this.airport_returns.put(airport_id, new TreeSet<ReturnFlight>());
} // FOR
// Data Generation Thread
// Ok, hang on tight. We are going to fork off a separate thread to generate our
// tuples because it's easier than trying to pick up where we left off every time
// That means that when hasNext() is called, it will block and poke this thread to start
// running. Once this thread has generate a new tuple, it will block itself and then
// poke the hasNext() thread. This is sort of like a hacky version of Python's yield
new Thread() {
public void run() {
try {
ReservationIterable.this.generateData();
} catch (Throwable ex) {
// System.err.println("Airport Customers:\n" + getAirportCustomerHistogram());
ReservationIterable.this.error = ex;
} finally {
if (debug.val) LOG.debug("Reservation generation thread is finished");
ReservationIterable.this.done = true;
}
} // run
}.start();
}
private void generateData() throws Exception {
if (debug.val) LOG.debug("Reservation data generation thread started");
Collection<Long> flight_customer_ids = new HashSet<Long>();
Collection<ReturnFlight> returning_customers = new ListOrderedSet<ReturnFlight>();
// Loop through the flights and generate reservations
for (long flight_id = 0, cnt = profile.num_flights; flight_id < cnt; flight_id++) {
FlightInfo flightInfo = SEATSLoader.this.flight_infos.remove(flight_id);
String depart_airport = flightInfo.depart_airport;
String arrive_airport = flightInfo.arrive_airport;
TimestampType depart_time = flightInfo.depart_time;
TimestampType arrive_time = flightInfo.arrive_time;
flight_customer_ids.clear();
// For each flight figure out which customers are returning
this.getReturningCustomers(returning_customers, flightInfo);
int booked_seats = SEATSConstants.FLIGHTS_NUM_SEATS - Math.max(SEATSConstants.FLIGHTS_RESERVED_SEATS,
flightInfo.seats_remaining);
if (trace.val) {
Map<String, Object> m = new ListOrderedMap<String, Object>();
m.put("Flight Id", flight_id);
m.put("Departure", String.format("%s / %s", depart_airport, depart_time));
m.put("Arrival", String.format("%s / %s", arrive_airport, arrive_time));
m.put("Booked Seats", booked_seats);
m.put(String.format("Returning Customers[%d]", returning_customers.size()), StringUtil.join("\n", returning_customers));
LOG.trace("Flight Information\n" + StringUtil.formatMaps(m));
}
for (int seatnum = SEATSConstants.FLIGHTS_RESERVED_SEATS; seatnum < booked_seats; seatnum++) {
Long customer_id = null;
Long airport_customer_cnt = profile.getCustomerIdCount(profile.getAirportId(depart_airport));
boolean local_customer = airport_customer_cnt != null && (flight_customer_ids.size() < airport_customer_cnt.intValue());
int tries = 2000;
ReturnFlight return_flight = null;
while (tries > 0) {
return_flight = null;
// Always book returning customers first
if (returning_customers.isEmpty() == false) {
return_flight = CollectionUtil.pop(returning_customers);
customer_id = return_flight.getCustomerId();
}
// New Outbound Reservation
// Prefer to use a customer based out of the local airport
else if (local_customer) {
customer_id = profile.getRandomCustomerId(); // depart_airport_id
}
// New Outbound Reservation
// We'll take anybody!
else {
customer_id = profile.getRandomCustomerId();
}
if (flight_customer_ids.contains(customer_id) == false) break;
tries--;
} // WHILE
assert(tries > 0) : String.format("Safety check! [local=%s]", local_customer);
// If this is return flight, then there's nothing extra that we need to do
if (return_flight != null) {
if (trace.val) LOG.trace("Booked return flight: " + return_flight + " [remaining=" + returning_customers.size() + "]");
// If it's a new outbound flight, then we will randomly decide when this customer will return (if at all)
} else {
if (rng.nextInt(100) < SEATSConstants.PROB_SINGLE_FLIGHT_RESERVATION) {
// Do nothing for now...
// Create a ReturnFlight object to record that this customer needs a flight
// back to their original depart airport
} else {
int return_days = rand_returns.nextInt();
return_flight = new ReturnFlight(customer_id, profile.getAirportId(depart_airport), depart_time, return_days);
this.airport_returns.get(arrive_airport).add(return_flight);
}
}
assert(customer_id != null) : "Null customer id on " + flight_id;
assert(flight_customer_ids.contains(customer_id) == false) : flight_id + " already contains " + customer_id;
flight_customer_ids.add(customer_id);
if (trace.val)
LOG.trace(String.format("New reservation ready. Adding to queue! [queueSize=%d]",
this.queue.size()));
Reservation r = new Reservation(1001, flight_id, customer_id, seatnum); // id doesn't matter
this.queue.put(r);
} // FOR (seats)
} // FOR (flights)
if (debug.val) LOG.debug("Reservation data generation thread is finished");
}
/**
* Return a list of the customers that need to return to their original
* location on this particular flight.
* @param flight_id
* @return
*/
private void getReturningCustomers(Collection<ReturnFlight> returning_customers, FlightInfo flightInfo) {
TimestampType flight_date = flightInfo.depart_time;
returning_customers.clear();
Set<ReturnFlight> returns = this.airport_returns.get(flightInfo.depart_airport);
if (!returns.isEmpty()) {
for (ReturnFlight return_flight : returns) {
if (return_flight.getReturnDate().compareTo(flight_date) > 0) break;
if (return_flight.getReturnAirportId() == profile.getAirportId(flightInfo.arrive_airport)) {
returning_customers.add(return_flight);
}
} // FOR
if (!returning_customers.isEmpty()) returns.removeAll(returning_customers);
}
}
@Override
protected boolean hasNext() {
if (trace.val) LOG.trace("hasNext() called");
this.current = null;
while (this.done == false || this.queue.isEmpty() == false) {
if (this.error != null)
throw new RuntimeException("Failed to generate Reservation records", this.error);
try {
this.current = this.queue.poll(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
throw new RuntimeException("Unexpected interruption!", ex);
}
if (this.current != null) return (true);
if (trace.val) LOG.trace("There were no new reservations. Let's try again!");
} // WHILE
return (false);
}
@Override
protected Object specialValue(long id, int columnIdx) {
assert(this.current != null);
Object value = null;
switch (columnIdx) {
// CUSTOMER ID
case (1): {
value = this.current.customer_id;
break;
}
// FLIGHT ID
case (2): {
value = this.current.flight_id;
// if (profile.getReservationUpcomingOffset() == null &&
// flight_id.isUpcoming(profile.getFlightStartDate(), profile.getFlightPastDays())) {
// profile.setReservationUpcomingOffset(id);
// }
break;
}
// SEAT
case (3): {
value = this.current.seatnum;
break;
}
// PRICE
case (4): {
value = (double)this.prices.nextInt();
break;
}
// BAD MOJO!
default:
assert(false) : "Unexpected special column index " + columnIdx;
} // SWITCH
return (value);
}
} // END CLASS
// -----------------------------------------------------------------
// FLIGHT IDS
// -----------------------------------------------------------------
// public Iterable<FlightId> getFlightIds() {
// return (new Iterable<FlightId>() {
// @Override
// public Iterator<FlightId> iterator() {
// return (new Iterator<FlightId>() {
// private int idx = 0;
// private final int cnt = seats_remaining.size();
//
// @Override
// public boolean hasNext() {
// return (idx < this.cnt);
// }
// @Override
// public FlightId next() {
// return (seats_remaining.get(this.idx++));
// }
// @Override
// public void remove() {
// // Not implemented
// }
// });
// }
// });
// }
// ----------------------------------------------------------------
// DISTANCE METHODS
// ----------------------------------------------------------------
public void setDistance(String airport0, String airport1, double distance) {
short short_distance = (short)Math.round(distance);
for (String a[] : new String[][] { {airport0, airport1}, { airport1, airport0 }}) {
if (!this.airport_distances.containsKey(a[0])) {
this.airport_distances.put(a[0], new HashMap<String, Short>());
}
this.airport_distances.get(a[0]).put(a[1], short_distance);
} // FOR
}
public Integer getDistance(String airport0, String airport1) {
assert(this.airport_distances.containsKey(airport0)) : "No distance entries for '" + airport0 + "'";
assert(this.airport_distances.get(airport0).containsKey(airport1)) : "No distance entries from '" + airport0 + "' to '" + airport1 + "'";
return ((int)this.airport_distances.get(airport0).get(airport1));
}
/**
* For the current depart+arrive airport destinations, calculate the estimated
* flight time and then add the to the departure time in order to come up with the
* expected arrival time.
* @param depart_airport
* @param arrive_airport
* @param depart_time
* @return
*/
public TimestampType calculateArrivalTime(String depart_airport, String arrive_airport, TimestampType depart_time) {
Integer distance = this.getDistance(depart_airport, arrive_airport);
assert(distance != null) :
String.format("The calculated distance between '%s' and '%s' is null",
depart_airport, arrive_airport);
long flight_time = Math.round(distance / SEATSConstants.FLIGHT_TRAVEL_RATE) * 3600000000l; // 60 sec * 60 min * 1,000,000
return (new TimestampType(depart_time.getTime() + flight_time));
}
}