Към съдържанието
wannacode

Хотелски резервации

Проблем

Една стая, две потвърждения

Малък хотел има четири стаи. Резервации идват отвсякъде: от сайта, от рецепцията, от туристически агенции. Една петъчна вечер две агенции резервират последната двойна стая в една и съща секунда. И двете получават потвърждение. В събота в стаята пристигат две семейства.

Ще направим система, в която сървърът пази стаите и резервациите, а клиентите пращат заявки по мрежата. Клиентът може:

  • да види свободните стаи;
  • да провери една стая;
  • да резервира стая на свое име;
  • да анулира своята резервация.

А когато двама клиенти резервират една стая едновременно, само единият успява. Това ще докажем с програма, която дава един и същ изход при всяко пускане.

Строим отдолу нагоре: моделът без мрежа, правилата и грешките, протоколът, сървърът и клиентът, едновременните заявки и накрая всичко в една програма.

По-точно, за любопитните
Моделът е един и същ на четирите езика на курса. В Python, както в Java, клиентите пращат редове текст по сокет и всеки клиент е в своя нишка. Стаите пази ключалка: with self._lock.

Идеята

Стаята

Стаята има номер, тип, цена за нощ и състояние: свободна или заета.

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 + " € на нощ, " + (free ? "свободна" : "заета");
}
}
  • Номерът, типът и цената не се менят. Мени се само състоянието, и то с два метода с ясни имена: occupy и release.
  • Кой е отседнал, стаята не знае. Стаята стига до всеки клиент, който пита, а името на госта не бива да излиза от хотела.
По-точно, за любопитните
Полетата започват с _: това значи „не пипай отвън“. Python не го забранява, но така се пише. Номерът, типът и цената се четат през @property без скоби (room.number), а смяна отвън (room.number = 5) спира с AttributeError, защото свойството няма setter. Типът на стаята е kind, защото type е вградена функция на Python. За повече от две състояния се ползва енумерация: class RoomState(Enum) от модула enum.

Виж го

Хотелът и резервацията

Хотелът държи стаите в списък, а гостите в речник „номер на стая → име“:
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 е частен (с _, по договорка): търси стаята по номер или хвърля изключение. Така никой отвън не може да вземе стая и да я заеме покрай правилата:

private Room find(int number) {
for (Room room : rooms) {
if (room.getNumber() == number) {
return room;
}
}
throw new RoomNotFoundException("Няма стая " + number + ".");
}

Резервацията проверява името, намира стаята и проверява дали е свободна. Едва тогава я заема:

public void reserve(int number, String guest) {
if (guest.isBlank()) {
throw new HotelException("Името на госта е задължително.");
}
Room room = find(number);
if (!room.isFree()) {
throw new RoomTakenException("Стая " + number + " вече е заета.");
}
room.occupy();
guests.put(number, guest);
}
Hotel hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
for (Room room : hotel.freeRooms()) {
System.out.println(room);
}
hotel.reserve(102, "Анна Петрова");
System.out.println("След резервацията: " + hotel.check(102));
System.out.println("Свободни стаи: " + hotel.freeRooms().size());
Изход
101, единична, 60 € на нощ, свободна
102, двойна, 85 € на нощ, свободна
201, двойна, 95 € на нощ, свободна
202, апартамент, 140 € на нощ, свободна
След резервацията: 102, двойна, 85 € на нощ, заета
Свободни стаи: 3

Идеята

Анулиране и правилата

Не всяка заявка е изпълнима: стаята е заета, номер 305 няма, гостът анулира чужда резервация. Това са откази по правилата, затова са свои изключения с общ родител:

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);
}
}

Анулирането проверява, че стаята е заета и че я анулира същият гост:

public void cancel(int number, String guest) {
Room room = find(number);
if (room.isFree()) {
throw new HotelException("Стая " + number + " не е резервирана.");
}
if (!guests.get(number).equals(guest)) {
throw new HotelException("Стая " + number + " е резервирана от друг гост.");
}
room.release();
guests.remove(number);
}

Условието на проекта иска системата да се провери в различни сценарии: резервация, проверка и анулиране, и когато са правилни, и когато не са. Всеки сценарий е ред в изхода:

Hotel hotel = new Hotel();
hotel.add(new Room(101, "единична", 60));
hotel.add(new Room(102, "двойна", 85));
hotel.add(new Room(201, "двойна", 95));
hotel.add(new Room(202, "апартамент", 140));
hotel.reserve(102, "Анна Петрова");
System.out.println("1. Свободна стая: " + hotel.check(102));
try {
hotel.reserve(102, "Борис Илиев");
} catch (RoomTakenException e) {
System.out.println("2. Заета стая: " + e.getMessage());
}
try {
hotel.cancel(102, "Борис Илиев");
} catch (HotelException e) {
System.out.println("3. Чужда резервация: " + e.getMessage());
}
hotel.cancel(102, "Анна Петрова");
System.out.println("4. Анулиране: " + hotel.check(102));
try {
hotel.cancel(102, "Анна Петрова");
} catch (HotelException e) {
System.out.println("5. Второ анулиране: " + e.getMessage());
}
try {
hotel.reserve(305, "Анна Петрова");
} catch (RoomNotFoundException e) {
System.out.println("6. Няма такава стая: " + e.getMessage());
}
try {
hotel.reserve(201, " ");
} catch (HotelException e) {
System.out.println("7. Без име: " + e.getMessage());
}
Изход
1. Свободна стая: 102, двойна, 85 € на нощ, заета
2. Заета стая: Стая 102 вече е заета.
3. Чужда резервация: Стая 102 е резервирана от друг гост.
4. Анулиране: 102, двойна, 85 € на нощ, свободна
5. Второ анулиране: Стая 102 не е резервирана.
6. Няма такава стая: Няма стая 305.
7. Без име: Името на госта е задължително.

Конкретното изключение казва какъв е проблемът. HotelError хваща всички откази наведнъж: сървърът ще хваща само него и ще го превръща в отговор за клиента.

По-точно, за любопитните
  • Проверени изключения няма: никой метод не обявява какво хвърля. Затова рецепцията ще ги хване на едно място.
  • Сценарии като тези са естествени тестове. С unittest всеки става метод на TestCase, а with self.assertRaises(RoomTakenError): около hotel.reserve(102, "Борис Илиев") проверява изключението.

Пробвай

Кой е в 201

Какво ще отпечата програмата?

hotel = Hotel()
hotel.add(Room(101, "единична", 60))
hotel.add(Room(102, "двойна", 85))
hotel.add(Room(201, "двойна", 95))
hotel.add(Room(202, "апартамент", 140))
hotel.reserve(101, "Анна Петрова")
hotel.reserve(201, "Борис Илиев")
hotel.cancel(101, "Анна Петрова")
try:
hotel.reserve(201, "Вера Колева")
print("Вера е в 201")
except RoomTakenError:
print("Отказ за Вера")
print(len(hotel.free_rooms()))
Отговор

Идеята

Протоколът: ред „КОМАНДА:текст“

Всяко съобщение е един ред КОМАНДА:текст, а сървърът отговаря с един ред:

Клиентът праща Сървърът отговаря
СВОБОДНИ ОК: и свободните стаи, разделени с ;
ПРОВЕРИ:102 ОК:102, двойна, 85 € на нощ, заета
РЕЗЕРВИРАЙ:102;Анна Петрова ОК:Стая 102 е твоя, Анна Петрова. или ГРЕШКА: и причината
АНУЛИРАЙ:102;Анна Петрова ОК:Резервацията за стая 102 е анулирана. или ГРЕШКА: и причината
КРАЙ ДОВИЖДАНЕ и сървърът затваря връзката

Думите на протокола са константи на едно място (в Python константата е договорка: главни букви и никой не я сменя):

class Protocol:
FREE = "СВОБОДНИ"
CHECK = "ПРОВЕРИ"
RESERVE = "РЕЗЕРВИРАЙ"
CANCEL = "АНУЛИРАЙ"
EXIT = "КРАЙ"
OK = "ОК"
ERROR = "ГРЕШКА"
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"Непозната команда {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}:Стая {number} е твоя, {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}:Резервацията за стая {number} е анулирана."
@staticmethod
def _number_and_guest(text):
parts = text.split(";", 1)
if len(parts) != 2:
raise HotelError("Пиши номер и име, например „102;Анна Петрова“.")
return parts
@staticmethod
def _number(text):
try:
return int(text)
except ValueError:
raise HotelError("Номерът на стаята е цяло число.")
@staticmethod
def _join(rooms):
if not rooms:
return "Няма свободни стаи."
return "; ".join(str(room) for room in rooms)
line.partition(":") дели реда на три: текста до първото :, самото : и останалото. Без : командата е целият ред, а текстът е празен. match сравнява командата с всяка константа, а case _ хваща всичко останало.

Рецепцията не помни нищо за клиента: всеки ред носи всичко нужно, дори името на госта. Затова една рецепция стига за всички клиенти. Проверява се без мрежа:

hotel = Hotel()
hotel.add(Room(101, "единична", 60))
hotel.add(Room(102, "двойна", 85))
hotel.add(Room(201, "двойна", 95))
hotel.add(Room(202, "апартамент", 140))
reception = Reception(hotel)
lines = [
"РЕЗЕРВИРАЙ:102;Анна Петрова",
"ПРОВЕРИ:102",
"РЕЗЕРВИРАЙ:102;Борис Илиев",
"АНУЛИРАЙ:102",
"ПРОВЕРИ:сто и две",
"ПОКАЖИ",
]
for line in lines:
print(f"> {line}")
print(reception.handle(line))
Изход
> РЕЗЕРВИРАЙ:102;Анна Петрова
ОК:Стая 102 е твоя, Анна Петрова.
> ПРОВЕРИ:102
ОК:102, двойна, 85 € на нощ, заета
> РЕЗЕРВИРАЙ:102;Борис Илиев
ГРЕШКА:Стая 102 вече е заета.
> АНУЛИРАЙ:102
ГРЕШКА:Пиши номер и име, например „102;Анна Петрова“.
> ПРОВЕРИ:сто и две
ГРЕШКА:Номерът на стаята е цяло число.
> ПОКАЖИ
ГРЕШКА:Непозната команда ПОКАЖИ.
По-точно, за любопитните
  • Командите могат да са енумерация: class Command(StrEnum) от модула enum. Тогава са отделен тип със затворен списък от стойности. В курса са константи, защото енумерации не сме учили.
  • Протоколът е без състояние: всеки ред е сам за себе си. Това го прави лесен за проверка и за повече клиенти. Ако клиентът трябваше първо да каже „аз съм Анна“, рецепцията щеше да е по една на клиент, както сесията в автокъщата.

Виж го

Сървърът

Всеки свързан клиент получава свой ClientHandler, а рецепцията е обща:

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"Връзката с клиента прекъсна: {e}")

makefile прави от сокета файл: редовете се четат с for, а се пишат с print(..., file=writer). flush=True праща реда веднага, а не когато буферът се напълни. with (...) затваря връзката и двата файла накрая, както и да е излязъл методът.

HotelServer приема клиентите в своя нишка и дава всеки на басейн от 10 нишки (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()
  • Десет клиенти се обслужват наведнъж. Единадесетият е приет и чака в опашката на басейна, докато някой не си тръгне.
  • И десетте нишки викат един и същ Hotel. Затова хотелът ще трябва да пази стаите си. Как, след малко.
  • Нишката, която приема клиентите, е daemon: тя не държи програмата жива, когато всичко останало е свършило.

Виж го

Клиентът

Клиентът праща ред и чете отговора. В Java send е два метода, с текст и без. В Python е един, а text=None значи, че текстът може да се пропусне. close() казва КРАЙ и затваря двата файла и връзката: сокетът се затваря наистина, едва когато и файловете от makefile са затворени.
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()

Сървърът и клиентът в една програма:

hotel = Hotel()
hotel.add(Room(101, "единична", 60))
hotel.add(Room(102, "двойна", 85))
hotel.add(Room(201, "двойна", 95))
hotel.add(Room(202, "апартамент", 140))
server = HotelServer(5321, hotel)
server.start()
client = HotelClient("localhost", 5321)
print(client.send(Protocol.RESERVE, "102;Анна Петрова"))
print(client.send(Protocol.CHECK, "102"))
print(client.send(Protocol.FREE))
print(client.send(Protocol.CANCEL, "102;Анна Петрова"))
print(client.send(Protocol.CHECK, "305"))
client.close()
server.stop()
Изход
ОК:Стая 102 е твоя, Анна Петрова.
ОК:102, двойна, 85 € на нощ, заета
ОК:101, единична, 60 € на нощ, свободна; 201, двойна, 95 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
ОК:Резервацията за стая 102 е анулирана.
ГРЕШКА:Няма стая 305.

Идеята

Двама за една стая

Резервацията е две стъпки: провери, че стаята е свободна, после я заеми. Ако две заявки се изпълняват едновременно, може да стане това:
Агенция А Агенция Б
1 проверява: 102 е свободна
2 проверява: 102 е свободна
3 заема 102 за своя гост
4 заема 102 за своя гост и записва него

И двете получават потвърждение. Няма грешка в нито един ред код, грешката е в това, че стъпките на двете заявки се преплитат.

Решението: в хотела влиза само една заявка наведнъж. Тогава „провери и заеми“ е неделимо: никой не може да влезе между проверката и заемането. В Python това прави ключалка в хотела, threading.RLock(), и with self._lock: около тялото на всеки публичен метод:
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("Името на госта е задължително.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"Стая {number} вече е заета.")
room.occupy()
self._guests[number] = guest

def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("Името на госта е задължително.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"Стая {number} вече е заета.")
room.occupy()
self._guests[number] = guest
Стъпка по стъпка, с думи
  1. Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
  2. Първо без with self._lock. Това е един възможен ред: заявката на Анна проверява и вижда „свободна“.
  3. Заявката на Борис проверява, преди Анна да е заела стаята: и тя вижда „свободна“.
  4. Анна заема стаята и записва името си.
  5. Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
  6. Отначало, с with self._lock: в хотела влиза само една нишка наведнъж.
  7. Да кажем, че първа е Анна: тя взима ключалката. Проверката и заемането са една стъпка, никой не влиза между тях.
  8. Борис стига до хотела и чака, докато ключалката се освободи.
  9. Ключалката е на Борис. Проверката вече вижда „заета“.
  10. RoomTakenError. Рецепцията го връща като ред с грешка, а Борис може да пита за друга стая.
  11. Програмата от урока брои успелите: по 1 за всяка стая. Кой е бил пръв, е различно при всяко пускане.

Доказателството: двама клиенти резервират 102 наведнъж, после двадесет агенции резервират 201 наведнъж. Отговорите се събират, а програмата печата колко са успели:

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, "единична", 60))
hotel.add(Room(102, "двойна", 85))
hotel.add(Room(201, "двойна", 95))
hotel.add(Room(202, "апартамент", 140))
server = HotelServer(5322, hotel)
server.start()
two = ["Анна Петрова", "Борис Илиев"]
print(f"Двама за стая 102, успешни: {race(5322, 102, two)}")
agencies = [f"Агенция {i + 1}" for i in range(20)]
print(f"Двадесет за стая 201, успешни: {race(5322, 201, agencies)}")
desk = HotelClient("localhost", 5322)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()
Изход
Двама за стая 102, успешни: 1
Двадесет за стая 201, успешни: 1
ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
Басейнът има толкова нишки, колкото са гостите, затова всички заявки тръгват наведнъж. map чака отговорите и ги връща в реда на гостите, а race брои успелите.

Кой е спечелил стаята, е различно при всяко пускане, затова програмата не го печата. Колко са успели, е винаги едно и също: по един за всяка стая.

По-точно, за любопитните
  • Без заключването програмата пак почти винаги би казала 1: прозорецът между проверката и заемането е малък. Но „почти винаги“ значи, че веднъж на хиляди резервации в стаята пристигат двама. Такъв бъг не се хваща с пускане, а с мислене: кои данни са общи и кой ги мени наведнъж. GIL на Python не пази „провери и заеми“: нишката може да бъде сменена между два реда. Ако между проверката и заемането има нещо бавно, например заявка към база данни, другата нишка влиза почти сигурно.
  • self._lock заключва целия хотел. Докато една нишка резервира 102, друга чака дори за 201. При четири стаи това е добре. При хиляди се заключва по стая, с ключалка за всяка. Ключалката е RLock, защото нишката, която я държи, може да я вземе пак. Това трябва в упражнението: смяната на стая вика cancel и reserve, които също заключват. С обикновен Lock вторият опит би чакал вечно: нишката чака сама себе си.
  • В истинска система стаите са в база данни и пазенето е там: транзакция или условие в самата заявка (UPDATE rooms SET free = false WHERE number = 102 AND free = true). Тогава важи и при няколко сървъра, а не само в една програма.

Пробвай

Заключено, но не съвсем

Колега раздели резервацията на два метода, и двата с with self._lock. Рецепцията вика първо проверката, после заемането. При много заявки наведнъж понякога пак двама получават една стая. На кой ред е грешката?

Избери реда с грешката.

Виж го

Целият хотел

Тестовите сценарии от условието, този път по мрежата, с двама клиенти, свързани наведнъж. Накрая двама резервират 201 в един миг:

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, "единична", 60))
hotel.add(Room(102, "двойна", 85))
hotel.add(Room(201, "двойна", 95))
hotel.add(Room(202, "апартамент", 140))
server = HotelServer(5323, hotel)
server.start()
anna = HotelClient("localhost", 5323)
boris = HotelClient("localhost", 5323)
say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова")
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")
say("Борис", boris, Protocol.CHECK, "102")
say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев")
say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова")
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")
say("Анна", anna, Protocol.CHECK, "305")
anna.close()
boris.close()
two = ["Анна Петрова", "Вера Колева"]
print(f"Двама за стая 201 наведнъж, успешни: {race(5323, 201, two)}")
desk = HotelClient("localhost", 5323)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()
Изход
Анна > РЕЗЕРВИРАЙ:102;Анна Петрова
Анна < ОК:Стая 102 е твоя, Анна Петрова.
Борис > РЕЗЕРВИРАЙ:102;Борис Илиев
Борис < ГРЕШКА:Стая 102 вече е заета.
Борис > ПРОВЕРИ:102
Борис < ОК:102, двойна, 85 € на нощ, заета
Борис > АНУЛИРАЙ:102;Борис Илиев
Борис < ГРЕШКА:Стая 102 е резервирана от друг гост.
Анна > АНУЛИРАЙ:102;Анна Петрова
Анна < ОК:Резервацията за стая 102 е анулирана.
Борис > РЕЗЕРВИРАЙ:102;Борис Илиев
Борис < ОК:Стая 102 е твоя, Борис Илиев.
Анна > ПРОВЕРИ:305
Анна < ГРЕШКА:Няма стая 305.
Двама за стая 201 наведнъж, успешни: 1
ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободна
По-точно, за любопитните

Цялата програма, от началото до края:

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 = "свободна" if self._free else "заета"
return f"{self._number}, {self._kind}, {self._price} € на нощ, {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("Името на госта е задължително.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"Стая {number} вече е заета.")
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"Стая {number} не е резервирана.")
if self._guests[number] != guest:
raise HotelError(f"Стая {number} е резервирана от друг гост.")
room.release()
del self._guests[number]
def _find(self, number):
for room in self._rooms:
if room.number == number:
return room
raise RoomNotFoundError(f"Няма стая {number}.")
class HotelError(Exception):
pass
class RoomNotFoundError(HotelError):
pass
class RoomTakenError(HotelError):
pass
class Protocol:
FREE = "СВОБОДНИ"
CHECK = "ПРОВЕРИ"
RESERVE = "РЕЗЕРВИРАЙ"
CANCEL = "АНУЛИРАЙ"
EXIT = "КРАЙ"
OK = "ОК"
ERROR = "ГРЕШКА"
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"Непозната команда {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}:Стая {number} е твоя, {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}:Резервацията за стая {number} е анулирана."
@staticmethod
def _number_and_guest(text):
parts = text.split(";", 1)
if len(parts) != 2:
raise HotelError("Пиши номер и име, например „102;Анна Петрова“.")
return parts
@staticmethod
def _number(text):
try:
return int(text)
except ValueError:
raise HotelError("Номерът на стаята е цяло число.")
@staticmethod
def _join(rooms):
if not rooms:
return "Няма свободни стаи."
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"Връзката с клиента прекъсна: {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, "единична", 60))
hotel.add(Room(102, "двойна", 85))
hotel.add(Room(201, "двойна", 95))
hotel.add(Room(202, "апартамент", 140))
server = HotelServer(5323, hotel)
server.start()
anna = HotelClient("localhost", 5323)
boris = HotelClient("localhost", 5323)
say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова")
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")
say("Борис", boris, Protocol.CHECK, "102")
say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев")
say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова")
say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")
say("Анна", anna, Protocol.CHECK, "305")
anna.close()
boris.close()
two = ["Анна Петрова", "Вера Колева"]
print(f"Двама за стая 201 наведнъж, успешни: {race(5323, 201, two)}")
desk = HotelClient("localhost", 5323)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()

Упражнение

Продължи сам

Работи с цялата програма от предишната карта.

1. Смяна на стая. Гостът се мести от една стая в друга с ПРЕМЕСТИ:102;202;Анна Петрова. Отговорът е ОК:Анна Петрова е в стая 202. Ако новата стая е заета, гостът остава в старата. Смяната е един метод в Hotel, в with self._lock: никой не бива да заеме новата стая между освобождаването на старата и заемането на новата.

Изход
ОК:Стая 102 е твоя, Анна Петрова.
ОК:Стая 201 е твоя, Борис Илиев.
ГРЕШКА:Стая 201 вече е заета.
ОК:Анна Петрова е в стая 202.
ОК:101, единична, 60 € на нощ, свободна; 102, двойна, 85 € на нощ, свободна

Още идеи

  • Нощувки и сума. Резервацията е за брой нощи, а потвърждението казва сумата: „3 нощи, 255 €“.
  • Дати. Стаята се резервира за дни, например от 14 до 16 декември. Две резервации за една стая не бива да се застъпват. Свободна ли е стаята, зависи от датите.
  • Моите резервации. МОИ:Анна Петрова казва кои стаи са на този гост.
  • Списък на чакащите. Ако стаята е заета, гостът се записва да чака. Когато се анулира, тя отива при първия в списъка.
  • Клиент от клавиатурата. Втора програма чете команди от клавиатурата с input() и ги праща на сървъра, пуснат в друг терминал.

Тест

Провери се

1. Две агенции резервират стая 102 в един и същ миг, а резервацията не е синхронизирана. Какво може да стане?
2. Проверката „свободна ли е“ и заемането на стаята са в два отделни синхронизирани метода. Достатъчно ли е?
3. Стая 102 е заета. Какво получава клиентът, който праща РЕЗЕРВИРАЙ:102;Борис Илиев?
4. Защо програмата печата колко резервации са успели, а не кой е спечелил стаята?
5. Защо имената на гостите са в речник в хотела, а не в стаята?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·