Skip to content
wannacode

Hotel reservations

Problem

One room, two confirmations

A small hotel has four rooms. Reservations come from everywhere: from the website, from the front desk, from travel agencies. One Friday evening, two agencies reserve the last double room in the same second. Both get a confirmation. On Saturday, two families arrive at the room.

We will build a system where the server keeps the rooms and the reservations, and clients send requests over the network. A client can:

  • see the free rooms;
  • check one room;
  • reserve a room in their own name;
  • cancel their own reservation.

And when two clients reserve the same room at the same time, only one of them succeeds. We will prove it with a program that gives the same output on every run.

We build from the bottom up: the model without a network, the rules and the errors, the protocol, the server and the client, the simultaneous requests, and finally everything in one program.

More precisely, if you are curious
The model is the same in all four languages of the course. In Python, as in Java, the clients send lines of text over a socket, and each client is in its own thread. A lock guards the rooms: with self._lock.

The idea

The room

A room has a number, a type, a price per night and a state: free or taken.

class Room {
private final int number;
private final String type;
private final int price;
private boolean free = true;
public Room(int number, String type, int price) {
this.number = number;
this.type = type;
this.price = price;
}
public int getNumber() {
return number;
}
public String getType() {
return type;
}
public int getPrice() {
return price;
}
public boolean isFree() {
return free;
}
public void occupy() {
free = false;
}
public void release() {
free = true;
}
@Override
public String toString() {
return number + ", " + type + ", €" + price + " per night, " + (free ? "free" : "taken");
}
}
  • The number, the type and the price do not change. Only the state changes, and only through two methods with clear names: occupy and release.
  • The room does not know who is staying in it. The room goes out to every client who asks, and the guest's name should not leave the hotel.
More precisely, if you are curious
The fields start with _: that means "do not touch from outside". Python does not forbid it, but that is the convention. The number, the type and the price are read through @property without parentheses (room.number), and changing one from outside (room.number = 5) stops with AttributeError, because the property has no setter. The type of the room is kind, because type is a built-in function in Python. For more than two states you use an enumeration: class RoomState(Enum) from the enum module.

See it

The hotel and the reservation

The hotel keeps the rooms in a list and the guests in a dictionary "room number → name":
private final List<Room> rooms = new ArrayList<>();
private final Map<Integer, String> guests = new HashMap<>();
public void add(Room room) {
rooms.add(room);
}
public List<Room> freeRooms() {
return rooms.stream().filter(Room::isFree).toList();
}
public Room check(int number) {
return find(number);
}

_find is private (with _, by convention): it looks up the room by number or throws an exception. That way nobody from outside can grab a room and take it around the rules:

private Room find(int number) {
for (Room room : rooms) {
if (room.getNumber() == number) {
return room;
}
}
throw new RoomNotFoundException("There is no room " + number + ".");
}

The reservation checks the name, finds the room and checks whether it is free. Only then does it take it:

public void reserve(int number, String guest) {
if (guest.isBlank()) {
throw new HotelException("The guest's name is required.");
}
Room room = find(number);
if (!room.isFree()) {
throw new RoomTakenException("Room " + number + " is already taken.");
}
room.occupy();
guests.put(number, guest);
}
Hotel hotel = new Hotel();
hotel.add(new Room(101, "single", 60));
hotel.add(new Room(102, "double", 85));
hotel.add(new Room(201, "double", 95));
hotel.add(new Room(202, "suite", 140));
for (Room room : hotel.freeRooms()) {
System.out.println(room);
}
hotel.reserve(102, "Anna Petrova");
System.out.println("After the reservation: " + hotel.check(102));
System.out.println("Free rooms: " + hotel.freeRooms().size());
Output
101, single, €60 per night, free
102, double, €85 per night, free
201, double, €95 per night, free
202, suite, €140 per night, free
After the reservation: 102, double, €85 per night, taken
Free rooms: 3

The idea

Canceling and the rules

Not every request can be carried out: the room is taken, there is no room 305, a guest cancels someone else's reservation. These are refusals by the rules, so they are your own exceptions with a common parent:

class HotelException extends RuntimeException {
public HotelException(String message) {
super(message);
}
}
class RoomNotFoundException extends HotelException {
public RoomNotFoundException(String message) {
super(message);
}
}
class RoomTakenException extends HotelException {
public RoomTakenException(String message) {
super(message);
}
}

Canceling checks that the room is taken and that the same guest is canceling it:

public void cancel(int number, String guest) {
Room room = find(number);
if (room.isFree()) {
throw new HotelException("Room " + number + " is not reserved.");
}
if (!guests.get(number).equals(guest)) {
throw new HotelException("Room " + number + " is reserved by another guest.");
}
room.release();
guests.remove(number);
}

The project brief asks for the system to be checked in different scenarios: reserving, checking and canceling, both when they are valid and when they are not. Each scenario is a line in the output:

Hotel hotel = new Hotel();
hotel.add(new Room(101, "single", 60));
hotel.add(new Room(102, "double", 85));
hotel.add(new Room(201, "double", 95));
hotel.add(new Room(202, "suite", 140));
hotel.reserve(102, "Anna Petrova");
System.out.println("1. Free room: " + hotel.check(102));
try {
hotel.reserve(102, "Boris Iliev");
} catch (RoomTakenException e) {
System.out.println("2. Taken room: " + e.getMessage());
}
try {
hotel.cancel(102, "Boris Iliev");
} catch (HotelException e) {
System.out.println("3. Someone else's reservation: " + e.getMessage());
}
hotel.cancel(102, "Anna Petrova");
System.out.println("4. Cancellation: " + hotel.check(102));
try {
hotel.cancel(102, "Anna Petrova");
} catch (HotelException e) {
System.out.println("5. Second cancellation: " + e.getMessage());
}
try {
hotel.reserve(305, "Anna Petrova");
} catch (RoomNotFoundException e) {
System.out.println("6. No such room: " + e.getMessage());
}
try {
hotel.reserve(201, " ");
} catch (HotelException e) {
System.out.println("7. No name: " + e.getMessage());
}
Output
1. Free room: 102, double, €85 per night, taken
2. Taken room: Room 102 is already taken.
3. Someone else's reservation: Room 102 is reserved by another guest.
4. Cancellation: 102, double, €85 per night, free
5. Second cancellation: Room 102 is not reserved.
6. No such room: There is no room 305.
7. No name: The guest's name is required.

The specific exception says what the problem is. HotelError catches all refusals at once: the server will catch only that one and turn it into a reply for the client.

More precisely, if you are curious
  • There are no checked exceptions: no method declares what it throws. So the reception will catch them in one place.
  • Scenarios like these are natural tests. With unittest each one becomes a method of a TestCase, and with self.assertRaises(RoomTakenError): around hotel.reserve(102, "Boris Iliev") checks the exception.

Try it

Who is in 201

What will the program print?

hotel = Hotel()
hotel.add(Room(101, "single", 60))
hotel.add(Room(102, "double", 85))
hotel.add(Room(201, "double", 95))
hotel.add(Room(202, "suite", 140))
hotel.reserve(101, "Anna Petrova")
hotel.reserve(201, "Boris Iliev")
hotel.cancel(101, "Anna Petrova")
try:
hotel.reserve(201, "Vera Koleva")
print("Vera is in 201")
except RoomTakenError:
print("Vera is turned away")
print(len(hotel.free_rooms()))
Answer

The idea

The protocol: a "COMMAND:text" line

Every message is one line COMMAND:text, and the server answers with one line:

The client sends The server answers
FREE OK: and the free rooms, separated by ;
CHECK:102 OK:102, double, €85 per night, taken
RESERVE:102;Anna Petrova OK:Room 102 is yours, Anna Petrova. or ERROR: and the reason
CANCEL:102;Anna Petrova OK:The reservation for room 102 is canceled. or ERROR: and the reason
EXIT BYE, and the server closes the connection

The words of the protocol are constants in one place (in Python a constant is a convention: capital letters, and nobody changes it):

class Protocol:
FREE = "FREE"
CHECK = "CHECK"
RESERVE = "RESERVE"
CANCEL = "CANCEL"
EXIT = "EXIT"
OK = "OK"
ERROR = "ERROR"
BYE = "BYE"

The reception translates a protocol line into a call to the hotel and back:

class Reception:
def __init__(self, hotel):
self._hotel = hotel
...
def handle(self, line):
command, _, text = line.partition(":")
try:
match command:
case Protocol.FREE:
return f"{Protocol.OK}:{self._join(self._hotel.free_rooms())}"
case Protocol.CHECK:
return f"{Protocol.OK}:{self._hotel.check(self._number(text))}"
case Protocol.RESERVE:
return self._reserve(text)
case Protocol.CANCEL:
return self._cancel(text)
case _:
raise HotelError(f"Unknown command {command}.")
except HotelError as e:
return f"{Protocol.ERROR}:{e}"
def _reserve(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.reserve(number, guest)
return f"{Protocol.OK}:Room {number} is yours, {guest}."
def _cancel(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.cancel(number, guest)
return f"{Protocol.OK}:The reservation for room {number} is canceled."
@staticmethod
def _number_and_guest(text):
parts = text.split(";", 1)
if len(parts) != 2:
raise HotelError("Write a number and a name, for example '102;Anna Petrova'.")
return parts
@staticmethod
def _number(text):
try:
return int(text)
except ValueError:
raise HotelError("The room number must be a whole number.")
@staticmethod
def _join(rooms):
if not rooms:
return "There are no free rooms."
return "; ".join(str(room) for room in rooms)
line.partition(":") splits the line into three: the text up to the first :, the : itself and the rest. Without a :, the command is the whole line and the text is empty. match compares the command with each constant, and case _ catches everything else.

The reception remembers nothing about the client: every line carries everything it needs, even the guest's name. That is why one reception is enough for all clients. You can check it without a network:

hotel = Hotel()
hotel.add(Room(101, "single", 60))
hotel.add(Room(102, "double", 85))
hotel.add(Room(201, "double", 95))
hotel.add(Room(202, "suite", 140))
reception = Reception(hotel)
lines = [
"RESERVE:102;Anna Petrova",
"CHECK:102",
"RESERVE:102;Boris Iliev",
"CANCEL:102",
"CHECK:one hundred two",
"SHOW",
]
for line in lines:
print(f"> {line}")
print(reception.handle(line))
Output
> RESERVE:102;Anna Petrova
OK:Room 102 is yours, Anna Petrova.
> CHECK:102
OK:102, double, €85 per night, taken
> RESERVE:102;Boris Iliev
ERROR:Room 102 is already taken.
> CANCEL:102
ERROR:Write a number and a name, for example '102;Anna Petrova'.
> CHECK:one hundred two
ERROR:The room number must be a whole number.
> SHOW
ERROR:Unknown command SHOW.
More precisely, if you are curious
  • The commands could be an enumeration: class Command(StrEnum) from the enum module. Then they are a separate type with a closed list of values. In the course they are constants, because we have not covered enumerations.
  • The protocol is stateless: every line stands on its own. That makes it easy to check and easy to serve more clients. If the client first had to say "I am Anna", there would have to be one reception per client, like the session in the car dealership.

See it

The server

Each connected client gets its own ClientHandler, and the reception is shared:

class ClientHandler:
def __init__(self, connection, reception):
self._connection = connection
self._reception = reception
def run(self):
try:
with (
self._connection,
self._connection.makefile("r", encoding="utf-8") as reader,
self._connection.makefile("w", encoding="utf-8") as writer,
):
for line in reader:
line = line.rstrip("\n")
if line == Protocol.EXIT:
break
print(self._reception.handle(line), file=writer, flush=True)
print(Protocol.BYE, file=writer, flush=True)
except OSError as e:
print(f"The connection to the client was lost: {e}")

makefile turns the socket into a file: the lines are read with for and written with print(..., file=writer). flush=True sends the line right away, not when the buffer fills up. with (...) closes the connection and both files at the end, however the method has exited.

HotelServer accepts the clients in its own thread and hands each one to a pool of 10 threads (ThreadPoolExecutor):

class HotelServer:
def __init__(self, port, hotel):
self._server_socket = socket.create_server(("localhost", port))
self._pool = ThreadPoolExecutor(max_workers=10)
self._reception = Reception(hotel)
def start(self):
acceptor = threading.Thread(target=self._accept_all, daemon=True)
acceptor.start()
def _accept_all(self):
while True:
try:
client, _ = self._server_socket.accept()
except OSError:
return
self._pool.submit(ClientHandler(client, self._reception).run)
def stop(self):
self._server_socket.close()
self._pool.shutdown()
  • Ten clients are served at once. The eleventh is accepted and waits in the pool's queue until someone leaves.
  • All ten threads call the same Hotel. So the hotel will have to guard its rooms. How, in a moment.
  • The thread that accepts the clients is a daemon: it does not keep the program alive when everything else is done.

See it

The client

The client sends a line and reads the reply. In Java send is two methods, with text and without. In Python it is one, and text=None means the text can be left out. close() says EXIT and closes both files and the connection: the socket really closes only when the files from makefile are closed too.
class HotelClient:
def __init__(self, host, port):
self._connection = socket.create_connection((host, port))
self._in = self._connection.makefile("r", encoding="utf-8")
self._out = self._connection.makefile("w", encoding="utf-8")
def send(self, command, text=None):
line = command if text is None else f"{command}:{text}"
print(line, file=self._out, flush=True)
return self._in.readline().rstrip("\n")
def close(self):
self.send(Protocol.EXIT)
self._in.close()
self._out.close()
self._connection.close()

The server and the client in one program:

hotel = Hotel()
hotel.add(Room(101, "single", 60))
hotel.add(Room(102, "double", 85))
hotel.add(Room(201, "double", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5331, hotel)
server.start()
client = HotelClient("localhost", 5331)
print(client.send(Protocol.RESERVE, "102;Anna Petrova"))
print(client.send(Protocol.CHECK, "102"))
print(client.send(Protocol.FREE))
print(client.send(Protocol.CANCEL, "102;Anna Petrova"))
print(client.send(Protocol.CHECK, "305"))
client.close()
server.stop()
Output
OK:Room 102 is yours, Anna Petrova.
OK:102, double, €85 per night, taken
OK:101, single, €60 per night, free; 201, double, €95 per night, free; 202, suite, €140 per night, free
OK:The reservation for room 102 is canceled.
ERROR:There is no room 305.

The idea

Two for one room

A reservation is two steps: check that the room is free, then take it. If two requests run at the same time, this can happen:
Agency A Agency B
1 checks: 102 is free
2 checks: 102 is free
3 takes 102 for its guest
4 takes 102 for its guest and records that guest

Both get a confirmation. There is no mistake in any single line of code. The mistake is that the steps of the two requests interleave.

The fix: only one request at a time gets into the hotel. Then "check and take" is atomic: nobody can get in between the check and taking the room. In Python, a lock in the hotel does this, threading.RLock(), with with self._lock: around the body of every public method:
def __init__(self):
self._rooms = []
self._guests = {}
self._lock = threading.RLock()
def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("The guest's name is required.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"Room {number} is already taken.")
room.occupy()
self._guests[number] = guest

def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("The guest's name is required.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"Room {number} is already taken.")
room.occupy()
self._guests[number] = guest
Step by step, in words
  1. Room 102 is free. Anna Petrova and Boris Iliev reserve it at the same moment.
  2. First without with self._lock. This is one possible order: Anna's request checks and sees "free".
  3. Boris's request checks before Anna has taken the room: it sees "free" too.
  4. Anna takes the room and records her name.
  5. Boris takes the room too and records his name. Both have a confirmation for the same room.
  6. From the start, with with self._lock: only one thread at a time gets into the hotel.
  7. Say Anna is first: she takes the lock. The check and the taking are one step, and nobody gets in between them.
  8. Boris reaches the hotel and waits until the lock is free.
  9. Boris has the lock. Now the check sees "taken".
  10. RoomTakenError. The reception sends it back as an error line, and Boris can ask for another room.
  11. The program from the lesson counts the ones that succeed: 1 for each room. Who was first is different on every run.

The proof: two clients reserve 102 at once, then twenty agencies reserve 201 at once. The replies are collected, and the program prints how many succeeded:

def reserve(port, room, guest):
try:
client = HotelClient("localhost", port)
reply = client.send(Protocol.RESERVE, f"{room};{guest}")
client.close()
return reply
except OSError as e:
return f"{Protocol.ERROR}:{e}"
def race(port, room, guests):
with ThreadPoolExecutor(max_workers=len(guests)) as clients:
replies = list(clients.map(lambda guest: reserve(port, room, guest), guests))
reserved = 0
for reply in replies:
if reply.startswith(Protocol.OK):
reserved += 1
return reserved
hotel = Hotel()
hotel.add(Room(101, "single", 60))
hotel.add(Room(102, "double", 85))
hotel.add(Room(201, "double", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5332, hotel)
server.start()
two = ["Anna Petrova", "Boris Iliev"]
print(f"Two for room 102, succeeded: {race(5332, 102, two)}")
agencies = [f"Agency {i + 1}" for i in range(20)]
print(f"Twenty for room 201, succeeded: {race(5332, 201, agencies)}")
desk = HotelClient("localhost", 5332)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()
Output
Two for room 102, succeeded: 1
Twenty for room 201, succeeded: 1
OK:101, single, €60 per night, free; 202, suite, €140 per night, free
The pool has as many threads as there are guests, so all the requests start at once. map waits for the replies and returns them in the order of the guests, and race counts the ones that succeeded.

Who won the room is different on every run, so the program does not print it. How many succeeded is always the same: one for each room.

More precisely, if you are curious
  • Without the locking the program would still almost always say 1: the window between the check and taking the room is small. But "almost always" means that once in thousands of reservations two guests arrive at the room. You do not catch a bug like that by running the program, but by thinking: which data is shared, and who changes it at the same time. Python's GIL does not protect "check and take": the thread can be switched between two lines. If there is something slow between the check and taking the room, such as a database query, the other thread gets in almost for sure.
  • self._lock locks the whole hotel. While one thread reserves 102, another waits even for 201. With four rooms that is fine. With thousands you lock per room, with a lock for each one. The lock is an RLock because the thread that holds it can take it again. The exercise needs that: changing rooms calls cancel and reserve, which lock too. With a plain Lock the second attempt would wait forever: the thread would be waiting for itself.
  • In a real system the rooms are in a database, and the guarding happens there: a transaction or a condition in the query itself (UPDATE rooms SET free = false WHERE number = 102 AND free = true). Then it holds even with several servers, not only inside one program.

Try it

Locked, but not quite

A colleague split the reservation into two methods, both with with self._lock. The reception calls the check first, then the taking. With many requests at once, two people still sometimes get the same room. Which line is the bug on?

Pick the line with the bug.

See it

The whole hotel

The test scenarios from the brief, this time over the network, with two clients connected at once. At the end two people reserve 201 at the same moment:

def say(who, client, command, text):
reply = client.send(command, text)
print(f"{who} > {command}:{text}")
print(f"{who} < {reply}")
hotel = Hotel()
hotel.add(Room(101, "single", 60))
hotel.add(Room(102, "double", 85))
hotel.add(Room(201, "double", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5333, hotel)
server.start()
anna = HotelClient("localhost", 5333)
boris = HotelClient("localhost", 5333)
say("Anna", anna, Protocol.RESERVE, "102;Anna Petrova")
say("Boris", boris, Protocol.RESERVE, "102;Boris Iliev")
say("Boris", boris, Protocol.CHECK, "102")
say("Boris", boris, Protocol.CANCEL, "102;Boris Iliev")
say("Anna", anna, Protocol.CANCEL, "102;Anna Petrova")
say("Boris", boris, Protocol.RESERVE, "102;Boris Iliev")
say("Anna", anna, Protocol.CHECK, "305")
anna.close()
boris.close()
two = ["Anna Petrova", "Vera Koleva"]
print(f"Two for room 201 at once, succeeded: {race(5333, 201, two)}")
desk = HotelClient("localhost", 5333)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()
Output
Anna > RESERVE:102;Anna Petrova
Anna < OK:Room 102 is yours, Anna Petrova.
Boris > RESERVE:102;Boris Iliev
Boris < ERROR:Room 102 is already taken.
Boris > CHECK:102
Boris < OK:102, double, €85 per night, taken
Boris > CANCEL:102;Boris Iliev
Boris < ERROR:Room 102 is reserved by another guest.
Anna > CANCEL:102;Anna Petrova
Anna < OK:The reservation for room 102 is canceled.
Boris > RESERVE:102;Boris Iliev
Boris < OK:Room 102 is yours, Boris Iliev.
Anna > CHECK:305
Anna < ERROR:There is no room 305.
Two for room 201 at once, succeeded: 1
OK:101, single, €60 per night, free; 202, suite, €140 per night, free
More precisely, if you are curious

The whole program, from start to finish:

hotel.py
import socket
import threading
from concurrent.futures import ThreadPoolExecutor
class Room:
def __init__(self, number, kind, price):
self._number = number
self._kind = kind
self._price = price
self._free = True
@property
def number(self):
return self._number
@property
def kind(self):
return self._kind
@property
def price(self):
return self._price
@property
def is_free(self):
return self._free
def occupy(self):
self._free = False
def release(self):
self._free = True
def __str__(self):
state = "free" if self._free else "taken"
return f"{self._number}, {self._kind}, €{self._price} per night, {state}"
class Hotel:
def __init__(self):
self._rooms = []
self._guests = {}
self._lock = threading.RLock()
def add(self, room):
with self._lock:
self._rooms.append(room)
def free_rooms(self):
with self._lock:
return [room for room in self._rooms if room.is_free]
def check(self, number):
with self._lock:
return self._find(number)
def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("The guest's name is required.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"Room {number} is already taken.")
room.occupy()
self._guests[number] = guest
def cancel(self, number, guest):
with self._lock:
room = self._find(number)
if room.is_free:
raise HotelError(f"Room {number} is not reserved.")
if self._guests[number] != guest:
raise HotelError(f"Room {number} is reserved by another guest.")
room.release()
del self._guests[number]
def _find(self, number):
for room in self._rooms:
if room.number == number:
return room
raise RoomNotFoundError(f"There is no room {number}.")
class HotelError(Exception):
pass
class RoomNotFoundError(HotelError):
pass
class RoomTakenError(HotelError):
pass
class Protocol:
FREE = "FREE"
CHECK = "CHECK"
RESERVE = "RESERVE"
CANCEL = "CANCEL"
EXIT = "EXIT"
OK = "OK"
ERROR = "ERROR"
BYE = "BYE"
class Reception:
def __init__(self, hotel):
self._hotel = hotel
def handle(self, line):
command, _, text = line.partition(":")
try:
match command:
case Protocol.FREE:
return f"{Protocol.OK}:{self._join(self._hotel.free_rooms())}"
case Protocol.CHECK:
return f"{Protocol.OK}:{self._hotel.check(self._number(text))}"
case Protocol.RESERVE:
return self._reserve(text)
case Protocol.CANCEL:
return self._cancel(text)
case _:
raise HotelError(f"Unknown command {command}.")
except HotelError as e:
return f"{Protocol.ERROR}:{e}"
def _reserve(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.reserve(number, guest)
return f"{Protocol.OK}:Room {number} is yours, {guest}."
def _cancel(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.cancel(number, guest)
return f"{Protocol.OK}:The reservation for room {number} is canceled."
@staticmethod
def _number_and_guest(text):
parts = text.split(";", 1)
if len(parts) != 2:
raise HotelError("Write a number and a name, for example '102;Anna Petrova'.")
return parts
@staticmethod
def _number(text):
try:
return int(text)
except ValueError:
raise HotelError("The room number must be a whole number.")
@staticmethod
def _join(rooms):
if not rooms:
return "There are no free rooms."
return "; ".join(str(room) for room in rooms)
class ClientHandler:
def __init__(self, connection, reception):
self._connection = connection
self._reception = reception
def run(self):
try:
with (
self._connection,
self._connection.makefile("r", encoding="utf-8") as reader,
self._connection.makefile("w", encoding="utf-8") as writer,
):
for line in reader:
line = line.rstrip("\n")
if line == Protocol.EXIT:
break
print(self._reception.handle(line), file=writer, flush=True)
print(Protocol.BYE, file=writer, flush=True)
except OSError as e:
print(f"The connection to the client was lost: {e}")
class HotelServer:
def __init__(self, port, hotel):
self._server_socket = socket.create_server(("localhost", port))
self._pool = ThreadPoolExecutor(max_workers=10)
self._reception = Reception(hotel)
def start(self):
acceptor = threading.Thread(target=self._accept_all, daemon=True)
acceptor.start()
def _accept_all(self):
while True:
try:
client, _ = self._server_socket.accept()
except OSError:
return
self._pool.submit(ClientHandler(client, self._reception).run)
def stop(self):
self._server_socket.close()
self._pool.shutdown()
class HotelClient:
def __init__(self, host, port):
self._connection = socket.create_connection((host, port))
self._in = self._connection.makefile("r", encoding="utf-8")
self._out = self._connection.makefile("w", encoding="utf-8")
def send(self, command, text=None):
line = command if text is None else f"{command}:{text}"
print(line, file=self._out, flush=True)
return self._in.readline().rstrip("\n")
def close(self):
self.send(Protocol.EXIT)
self._in.close()
self._out.close()
self._connection.close()
def say(who, client, command, text):
reply = client.send(command, text)
print(f"{who} > {command}:{text}")
print(f"{who} < {reply}")
def reserve(port, room, guest):
try:
client = HotelClient("localhost", port)
reply = client.send(Protocol.RESERVE, f"{room};{guest}")
client.close()
return reply
except OSError as e:
return f"{Protocol.ERROR}:{e}"
def race(port, room, guests):
with ThreadPoolExecutor(max_workers=len(guests)) as clients:
replies = list(clients.map(lambda guest: reserve(port, room, guest), guests))
reserved = 0
for reply in replies:
if reply.startswith(Protocol.OK):
reserved += 1
return reserved
hotel = Hotel()
hotel.add(Room(101, "single", 60))
hotel.add(Room(102, "double", 85))
hotel.add(Room(201, "double", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5333, hotel)
server.start()
anna = HotelClient("localhost", 5333)
boris = HotelClient("localhost", 5333)
say("Anna", anna, Protocol.RESERVE, "102;Anna Petrova")
say("Boris", boris, Protocol.RESERVE, "102;Boris Iliev")
say("Boris", boris, Protocol.CHECK, "102")
say("Boris", boris, Protocol.CANCEL, "102;Boris Iliev")
say("Anna", anna, Protocol.CANCEL, "102;Anna Petrova")
say("Boris", boris, Protocol.RESERVE, "102;Boris Iliev")
say("Anna", anna, Protocol.CHECK, "305")
anna.close()
boris.close()
two = ["Anna Petrova", "Vera Koleva"]
print(f"Two for room 201 at once, succeeded: {race(5333, 201, two)}")
desk = HotelClient("localhost", 5333)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()

Exercise

Keep going on your own

Work with the whole program from the previous card.

1. Changing rooms. The guest moves from one room to another with MOVE:102;202;Anna Petrova. The reply is OK:Anna Petrova is in room 202. If the new room is taken, the guest stays in the old one. The move is one method in Hotel, in with self._lock: nobody should take the new room between freeing the old one and taking the new one.

Output
OK:Room 102 is yours, Anna Petrova.
OK:Room 201 is yours, Boris Iliev.
ERROR:Room 201 is already taken.
OK:Anna Petrova is in room 202.
OK:101, single, €60 per night, free; 102, double, €85 per night, free

More ideas

  • Nights and total. A reservation is for a number of nights, and the confirmation says the total: "3 nights, €255".
  • Dates. A room is reserved for days, for example from December 14 to 16. Two reservations for the same room must not overlap. Whether the room is free depends on the dates.
  • My reservations. MINE:Anna Petrova says which rooms belong to this guest.
  • Waiting list. If the room is taken, the guest signs up to wait. When it is canceled, it goes to the first one on the list.
  • A client from the keyboard. A second program reads commands from the keyboard with input() and sends them to the server, which runs in another terminal.

Test

Check yourself

1. Two agencies reserve room 102 at the same moment, and the reservation is not synchronized. What can happen?
2. The "is it free" check and taking the room are in two separate synchronized methods. Is that enough?
3. Room 102 is taken. What does the client that sends RESERVE:102;Boris Iliev get?
4. Why does the program print how many reservations succeeded, and not who won the room?
5. Why are the guests' names in a dictionary in the hotel, and not in the room?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·