/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* 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 callcenter;
import java.util.ArrayDeque;
import java.util.Date;
import java.util.Queue;
import java.util.Random;
/**
* Generator for fake call data.
*
* Generates pairs of events, begin calls and end calls.
*
* End calls are scheduled for future delivery.
*
*/
public class CallSimulator implements EventSource<CallEvent> {
CallCenterApp.CallCenterConfig config;
// random number generator with constant seed
final Random rand = new Random(0);
final DelayedQueue<CallEvent> delayedEvents = new DelayedQueue<>();
// used for pacing
long currentSystemMilliTimestamp = 0;
double targetEventsPerMillisecond;
long targetEventsThisMillisecond;
long eventsSoFarThisMillisecond;
// incrementing counter
long lastCallIdUsed = 0;
Queue<Integer> agentsAvailable = new ArrayDeque<>();
Queue<Long> phoneNumbersAvailable = new ArrayDeque<>();
CallSimulator(CallCenterApp.CallCenterConfig config) {
this.config = config;
targetEventsPerMillisecond = 5;
// generate agents
for (int i = 0; i < config.agents; i++) {
agentsAvailable.add(i);
}
// generate phone numbers
for (int i = 0; i < config.numbers; i++) {
// random area code between 200 and 799
long areaCode = rand.nextInt(600) + 200;
// random exchange between 200 and 999
long exhange = rand.nextInt(800) + 200;
// full random number
long phoneNo = areaCode * 10000000 + exhange * 10000 + rand.nextInt(9999);
phoneNumbersAvailable.add(phoneNo);
}
}
/**
* Generate a random call event with a duration.
*
* Reserves agent and phone number from the pool.
*/
CallEvent[] makeRandomEvent() {
long callId = ++lastCallIdUsed;
// get agentid
Integer agentId = agentsAvailable.poll();
if (agentId == null) {
return null;
}
// get phone number
Long phoneNo = phoneNumbersAvailable.poll();
assert(phoneNo != null);
// voltdb timestamp type uses micros from epoch
Date startTS = new Date(currentSystemMilliTimestamp);
long durationms = -1;
long meancalldurationms = config.meancalldurationseconds * 1000;
long maxcalldurationms = config.maxcalldurationseconds * 1000;
double stddev = meancalldurationms / 2.0;
// repeat until in the range (0..maxcalldurationms]
while ((durationms <= 0) || (durationms > maxcalldurationms)) {
durationms = (long) (rand.nextGaussian() * stddev) + meancalldurationms;
}
Date endTS = new Date(startTS.getTime() + durationms);
CallEvent[] event = new CallEvent[2];
event[0] = new CallEvent(callId, agentId, phoneNo, startTS, null);
event[1] = new CallEvent(callId, agentId, phoneNo, null, endTS);
// some debugging code
//System.out.println("Creating event with range:");
//System.out.println(new Date(startTS.getTime() / 1000));
//System.out.println(new Date(endTS.getTime() / 1000));
return event;
}
/**
* Return the next call event that is safe for delivery or null
* if there are no safe objects to deliver.
*
* Null response could mean empty, or could mean all objects
* are scheduled for the future.
*
* @param systemCurrentTimeMillis The current time.
* @return CallEvent
*/
@Override
public CallEvent next(long systemCurrentTimeMillis) {
// check for time passing
if (systemCurrentTimeMillis > currentSystemMilliTimestamp) {
// build a target for this 1ms window
long eventBacklog = targetEventsThisMillisecond - eventsSoFarThisMillisecond;
targetEventsThisMillisecond = (long) Math.floor(targetEventsPerMillisecond);
double targetFraction = targetEventsPerMillisecond - targetEventsThisMillisecond;
targetEventsThisMillisecond += (rand.nextDouble() <= targetFraction) ? 1 : 0;
targetEventsThisMillisecond += eventBacklog;
// reset counter for this 1ms window
eventsSoFarThisMillisecond = 0;
currentSystemMilliTimestamp = systemCurrentTimeMillis;
}
// drain scheduled events first
CallEvent callEvent = delayedEvents.nextReady(systemCurrentTimeMillis);
if (callEvent != null) {
// double check this is an end event
assert(callEvent.startTS == null);
assert(callEvent.endTS != null);
// return the agent/phone for this event to the available lists
agentsAvailable.add(callEvent.agentId);
phoneNumbersAvailable.add(callEvent.phoneNo);
validate();
return callEvent;
}
// check if we made all the target events for this 1ms window
if (targetEventsThisMillisecond == eventsSoFarThisMillisecond) {
validate();
return null;
}
// generate rando event (begin/end pair)
CallEvent[] event = makeRandomEvent();
// this means all agents are busy
if (event == null) {
validate();
return null;
}
// schedule the end event
long endTimeKey = event[1].endTS.getTime();
assert((endTimeKey - systemCurrentTimeMillis) < (config.maxcalldurationseconds * 1000));
delayedEvents.add(endTimeKey, event[1]);
eventsSoFarThisMillisecond++;
validate();
return event[0];
}
/**
* Ignore any scheduled delays and return events in
* schedule order until empty.
*/
@Override
public CallEvent drain() {
CallEvent callEvent = delayedEvents.drain();
if (callEvent == null) {
validate();
return null;
}
// double check this is an end event
assert(callEvent.startTS == null);
assert(callEvent.endTS != null);
// return the agent/phone for this event to the available lists
agentsAvailable.add(callEvent.agentId);
phoneNumbersAvailable.add(callEvent.phoneNo);
validate();
return callEvent;
}
/**
* Smoke check on validity of data structures.
* This was useful while getting the code right for this class,
* but it doesn't do much now, unless the code needs changes.
*/
private void validate() {
long delayedEventCount = delayedEvents.size();
long outstandingAgents = config.agents - agentsAvailable.size();
long outstandingPhones = config.numbers - phoneNumbersAvailable.size();
if (outstandingAgents != outstandingPhones) {
throw new RuntimeException(
String.format("outstandingAgents (%d) != outstandingPhones (%d)",
outstandingAgents, outstandingPhones));
}
if (outstandingAgents != delayedEventCount) {
throw new RuntimeException(
String.format("outstandingAgents (%d) != delayedEventCount (%d)",
outstandingAgents, delayedEventCount));
}
}
/**
* Debug statement to help users verify there are no lost or delayed events.
*/
void printSummary() {
System.out.printf("There are %d agents outstanding and %d phones. %d entries waiting to go.\n",
agentsAvailable.size(), phoneNumbersAvailable.size(), delayedEvents.size());
}
}