HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonModerate

Espresso Queue simulation

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
espressoqueuesimulation

Problem

I was asked to do a technical test with the following specification, but my solution was rejected and I was not selected for the available position (Junior to "normal" level, with 4 days of time to finish the test). Could you point out which areas need improvement in my solution?


In software company X, engineers work best when consuming one cup of
espresso an hour. The office espresso machine has a
first-come-first-serve queue that applies to everyone, except for
certain "super busy" engineers who are prioritized before
non-super-busy ones. Among competing super-busies the
first-come-first-serve principle applies again.


Please implement a simulator for this espresso machine. Input
parameters are number of engineers, the chance that an engineer
becomes super-busy in some unit of time, and for how long they stay
super-busy.

And my solution in Python 3.4: (GitHub version)

machine.py

```
import msvcrt # Windows-only non-blocking input. Universal would need a lot more things
import random
from classes.espresso import Espresso
from classes.queue import Queue
from classes.coffeeenum import CoffeeEnum
from classes.order import Order

# Launch the machine
num_engineers = 1705
chance_superbusy = 25 # percent
max_time_superbusy = 120 # minutes
espresso_machine = Espresso(num_engineers, chance_superbusy, max_time_superbusy)
espresso_machine.start()

# Setup a few things
queue = Queue()
queue_id = 0
coffee_name = CoffeeEnum()
superbusy = False

# Run the show!
espresso_machine.display_coffee_choices()
while True:
if queue.has_orders() and espresso_machine.is_ready():
espresso_machine.brew(queue.get_next(), queue)

# Read input to get new orders and enter superbusy mode
if msvcrt.kbhit():
# There's a chance the engineer is superbusy!
if random.randrange(0, 100) < chance_superbusy:
print("By surprise, you are now superbusy!")
superbusy = True

input = msvcrt.getch().decode("ascii").up

Solution


  1. Failed requirements



The program fails to implement the following details from the specification:

-
The specification says, "Input parameters are ..." but these parameters as implemented as global variables and not as inputs.

-
One of the parameters is "number of engineers", but the program makes no use of this number; it does not even represent the engineers at all.

-
Another parameter is "the chance that an engineer becomes super-busy in some unit of time" (my emphasis). But what the program implements is the chance that the current engineer becomes super-busy when a key is pressed.

-
The third parameter is "how long they stay super-busy" but in this implementation an engineer stops being super-busy as soon as they have placed an order.

  1. Other problems



-
There's no documentation.

-
It looks to me as though the specification is looking for a simulator in the sense of "computer model of a phenomenon" — a program which we could use to investigate questions like, "how does the typical queue length vary as we change the number of engineers?" But this implementation is an interactive simulation, which might not be so useful, because someone has to babysit it in order to get any results.

-
The program fails to abstract away the measurement of time. This means that if some event takes 15 seconds in the simulation then we have to wait 15 seconds in real life for it to happen. This means that we can't exploit the power of computers to simulate events thousands or millions of times faster than they happen.

-
The program contains lots of features that weren't in the specification, like the names of the engineers, the different types of coffee, the queue status report, and so on. If the specification had been fully implemented, this kind of extra detail can be fun, but in the circumstances it looks like a distraction. (Also, given that you picked names for the engineers, did they really all have to be male names?)

-
The program only runs on Windows!

-
There are lots of minor problems that indicate a lack of care and attention. There's an Order class, but it is never used; the Espresso class maintains is_running and brew_started_time members but these are not used; Espresso.__init__ takes parameters num_engineers, chance_superbusy, max_time_superbusy but these are just printed out and not otherwise used; the module classs/espresso.py imports Queue but does not use it.

  1. Model answer



Specifications rarely provide a complete description of a program. This means that it is nearly always useful to write down a design that lists and justifies the decisions you have made. Here are mine:

-
The specification omits some necessary parameters to the simulation. We need to add parameters for how long an engineer works before taking a coffee break and how long an espresso takes to brew.

-
I've chosen to abstract away the notion of time passing in the form of a Schedule class. This processes events in order by their timestamp, which is just a number. Justification: we don't have to deal with the complexity of operating system real-time interfaces; also, we can run the simulation at whatever speed we like.

-
The scheduler stores future events in a priority queue implemented as a min-heap using the heapq module. Justification: can add events and retrieve the earliest event in logarithmic time.

-
I've chosen to represent an event as a function of one argument (the time at which it happens). Justification: separation of concerns (the scheduler doesn't need to have any knowledge about the events); easily extensible with more types of event.

-
I've implemented the queue for the espresso machine as a map from engineer to the time they joined the queue. The priority of super-busy engineers over ordinarily busy engineers is implemented by making the engineers orderable by giving them a __lt__ method. Justification: this ensures that we can't make a mistake that leads to the engineer being on the queue multiple times; it also avoids the complexity that would result if I had two queues (an engineer might change super-busy status while in the queue and this would require them to change queue).

-
I've represented parameters like "how long an engineer works before taking a coffee break" in the form of random variables, implemented as functions of zero arguments returning a duration. Justification: real-world processes have variation in how long they take; the implementation gives us the flexibility to easily change the way durations vary in case we've misunderstood the simulation requirements.

-
The specification says, "chance that an engineer becomes super-busy in some unit of time". I understand this to mean that the time an engineer remains (ordinarily) busy is exponentially distributed, and so I've implemented it using random.expovariate.

Here's the implementation:

```
from collections.abc import Iterator
import heapq
from itertools import count
from random import expov

Code Snippets

from collections.abc import Iterator
import heapq
from itertools import count
from random import expovariate

class Schedule(Iterator):
    """An iterator yielding future events in order by time.

    An event is represented by a function taking one argument (the
    time at which the event happens). Schedule an event by calling the
    add method.

    Getting the next item from the iterator causes the earliest
    remaining event to be called.

    """
    def __init__(self):
        self._events = [] # Priority queue in the form of a min-heap
        self._seq = count() # Iterator yielding unique sequence ids

    def __next__(self):
        if not self._events:
            raise StopIteration
        time, _, event = heapq.heappop(self._events)
        return time, event(time)

    def add(self, time, event):
        # Priority queue entries must be orderable, but functions are
        # unorderable, so use a unique sequence number to break ties.
        # See https://docs.python.org/3/library/heapq.html
        heapq.heappush(self._events, (time, next(self._seq), event))

class Machine:
    """Model of an espresso machine with a queue of engineers. The
    constructor takes arguments:

    schedule -- a Schedule object managing the sequence of events
    brew_variate -- function returning time taken to brew an espresso

    """
    def __init__(self, schedule, brew_variate):
        self.schedule = schedule
        self.brew_variate = brew_variate
        self.brewing = False
        self._queue = dict()    # Map from engineer to time they joined queue

    def enqueue(self, time, engineer):
        """An engineer joins the queue for the espresso machine at time."""
        self._queue[engineer] = time
        self.brew(time)

    def brew(self, time):
        """Start brewing espresso at time (unless already brewing)."""
        if not self.brewing and self._queue:
            self.brewing = True
            self.schedule.add(time + self.brew_variate(), self.serve)

    def serve(self, time):
        """Serve espresso to the engineer at the head of the queue at time."""
        engineer, _ = min(self._queue.items())
        del self._queue[engineer]
        engineer.work(time)
        self.brewing = False
        self.brew(time)
        return ('Espresso served to {} ({} waiting)'
                .format(engineer, len(self._queue)))

class Engineer:
    """Model of an engineer. The constructor takes arguments:

    schedule -- a Schedule object managing the sequence of events    
    machine -- a Machine object representing the espresso machine
    time -- the time at which the engineer is created
    work_variate -- function returning time to work
    busy_variate -- function returning time to stay "busy"
    superbusy_variate -- function return time to stay "super-busy"

    """
    _ids = count()              # Iterator yielding engineer ids.

    def __init__(self, schedule, machine, time, work_variate, busy_variate,
                 su

Context

StackExchange Code Review Q#62603, answer score: 12

Revisions (0)

No revisions yet.