
Эта система пожарной сигнализации Nepo Dev является идеальной метафорой для Pub/Sub
3 июля 2025 г.Познакомьтесь с Дейвом, разработчиком, который получил контракт на пожарную сигнализацию отеля, потому что он был племянником генерального директора. Дейв никогда раньше не видел систему пожарной сигнализации, но он только что закончил проект обслуживания клиентов, где «Личные телефонные звонки увеличивают участие на 847%!» Естественно, Дейв думает: «Пожарная сигнализация - это просто обслуживание клиентов для чрезвычайных ситуаций!»
Его рассуждения: «Когда моя мама звонит мне напрямую, я всегда отвечаю. Когда уходит какой -то общий сигнал тревоги, я предполагаю, что это автомобильная тревога, и игнорировать его. Личное прикосновение - это ключ!»
Фаза 1: связь с точкой до пункта (фиаско телефонного звонка)
Блестящее решение Дэйва? Когда дым обнаруживается, система вызывает каждую комнату в отеле.
Комната 237 звонит: «Привет, на кухне есть дым, пожалуйста, эвакуируйтесь». Комната 301: «Привет, на кухне есть дым, пожалуйста, эвакуируйтесь». Все 847 номеров, один за другим. Дэйв горд - все получают сообщение!
Но вот где это становится грязно. Что происходит, когда на кухне появляется небольшой смазчик и курить в гараже? Система Дейва звонит в комнату 237: «Привет, на кухне есть дым».НажиматьЗатем сразу: «Привет, в гараже есть дым». Гость смущен - какая чрезвычайная ситуация? Что происходит?
Тем временем, в номере 512 есть гость, который глухо. Комната 623 проверила несколько часов назад, но телефон продолжает звонить. В номере 108 есть спальный ребенок, чьи родители сейчас в ярости. После третьей ложной тревоги в течение недели гости начинают отключать свои телефоны.
Это классическая связь с точки зрения-один отправитель, один приемник, повторяемый 847 раз. Это не масштабируется, это неэффективно, и раздражает всех участников.
Пожарный инспектор появляется: «Дейв, вы не можете этого сделать. Установите обычную пожарную сигнализацию». Дейв неохотно подчиняется.
Фаза 2: Коммуникация вещания (ядерный вариант)
Но Дэйв узнал из своих ошибок. "У меня есть лучшая идея!" Он объявляет.
Дейв устанавливает массивную пожарную сигнализацию, которая взрывается по всему отелю. Успех! Все это слышат! Но затем Дейв понимает, что эта система идеально подходит для объявлений. Закрытие бассейна? Пожарная сигнализация. Завтрак заканчивается за 10 минут? Пожарная сигнализация. Потерянный ребенок в вестибюле? Пожарная сигнализация. Йога класс начинается? Пожарная сигнализация.
Гости теперь эвакуируются для уведомлений о континентальном завтраке. Пожарная служба была вызвана 47 раз на этой неделе. Кто -то попытался проверить через экстренную выход.
Это передача коммуникации - одно сообщение для всех, хотят они этого или нет. Это похоже на использование общегородской экстренной системы вещания, чтобы объявить о вашей продаже гаража.
Фаза 3: Дейв обнаруживает что -то революционное
Третья попытка Дэйва: «Что, если у нас были разные интерком, для разных областей?» Он устанавливает динамики по всему отелю, но проводит их для разделения каналов:
- Область бассейна: «Закрытие бассейна за 10 минут, заканчивается сервисом полотенец»
- Ресторан Intercom: «Счастливого часа начинается, скоро закрывается кухня»
- Конференц -зал интерком: «Комната конференц -зала B доступен, пароль Wi -Fi Изменен»
- SPA Intercom: «Доступны встречи массажа, техническое обслуживание сауны в 15:00»
- Специфичные для пола интерком: «Уборка на полу 3, ледяная машина сломана на полу 7»
Гости естественным образом настраиваются на интерфейсы в областях, которые они на самом деле используют. Гости бассейна слышат обновления бассейна, ресторанные посетители получают информацию об обедах, участники конференции получают объявления о зале заседаний.
Что на самом деле построил Дэйв: Publish-Subscribe (Pub/sub)
Не осознавая этого, Дейв создалPublish-Subscribe System- Один из самых важных моделей в современной архитектуре программного обеспечения.
Вот как это работает:
- Издатели(Сотрудники отеля) Отправьте сообщения в конкретныетемыиликаналы(Обновления бассейна, новости ресторана и т. Д.)
- Подписчики(гости) Выберите, какие темы они хотят слушать
- Издатели не знают, кто слушает, подписчики не знают, кто посылает - они полностью отделены
Это N × M Communication: многие издатели могут отправлять многим подписчикам, но она организована и отфильтрована. Нет больше шума, больше не пропущенных сообщений, больше не звонить в каждую комнату индивидуально.
Почему это важно для вашего кода
Дейв случайно построил три фундаментальных моделей обмена сообщениями:
Точка-точка: Прямые связи между службами. Не масштабируется, когда вам нужно уведомлять несколько систем.
Транслировать: Одно сообщение всем. Создает шум и плотную связь.
Паб/суб: Издатели отправляют сообщения на темы/каналы, подписчики выбирают, что слушать. Масштабируемый, отдельный и эффективный.
В реальных системах, паб/sub решает те же проблемы, которые столкнулся с Дейвом:
- Электронная коммерция: Обновления инвентаризации перейдите к нескольким сервисам (рекомендации, аналитика, уведомления) без службы инвентаризации, зная, кто слушает
- Приложения чата: Сообщения, опубликованные по каналам, пользователи подписываются на разговоры, они в
- Микросервисы: Службы публикуют события (зарегистрированный пользователя, заказанный), которые другие службы могут реагировать на самостоятельно
Создание собственного паба/субсистемы (это проще, чем вы думаете)
Pub/sub не обязательно должен быть пугающим. Давайте построим систему производственного уровня, которой Дэйв будет гордиться:
from collections import defaultdict
from typing import Dict, List, Callable
class HotelPubSub:
def __init__(self):
self.channels: Dict[str, List[Callable]] = defaultdict(list)
def subscribe(self, channel: str, callback: Callable):
"""Guest subscribes to hotel updates"""
self.channels[channel].append(callback)
print(f"Subscribed to {channel}")
def publish(self, channel: str, message: str):
"""Hotel staff publishes updates"""
if channel in self.channels:
for callback in self.channels[channel]:
callback(message)
print(f"Published to {channel}: {message}")
# Dave's hotel in action
hotel = HotelPubSub()
# Guests subscribe to what they care about
def pool_guest(msg): print(f"🏊 Pool Guest: {msg}")
def restaurant_guest(msg): print(f"🍽️ Restaurant Guest: {msg}")
hotel.subscribe("pool", pool_guest)
hotel.subscribe("restaurant", restaurant_guest)
# Hotel publishes updates
hotel.publish("pool", "Pool closing in 10 minutes!")
hotel.publish("restaurant", "Happy hour starts now!")
Вот и все! Рабочий паб/суб -система в 20 строках.
Оптимизация с асинхронными очередями
Но что, если отель Дейва станет очень занятым? Наша простая версия блокирует издатели, когда подписчики медленные. Давайте добавим очередь:
import asyncio
from asyncio import Queue
from collections import defaultdict
class AsyncPubSub:
def __init__(self):
self.channels = defaultdict(list)
self.message_queue = Queue()
# Start background worker
asyncio.create_task(self._worker())
def subscribe(self, channel: str, callback):
self.channels[channel].append(callback)
async def publish(self, channel: str, message: str):
# Non-blocking publish - just queue it
await self.message_queue.put((channel, message))
async def _worker(self):
# Background worker processes messages
while True:
channel, message = await self.message_queue.get()
if channel in self.channels:
# Run all subscribers in parallel
tasks = [callback(message) for callback in self.channels[channel]]
await asyncio.gather(*tasks, return_exceptions=True)
# Dave's async hotel in action
async def demo():
hotel = AsyncPubSub()
# Fast and slow subscribers
async def fast_guest(msg):
await asyncio.sleep(0.1) # Quick processing
print(f"🏊 Fast: {msg}")
async def slow_guest(msg):
await asyncio.sleep(2.5) # Simulate slow database write
print(f"🍽️ Slow: {msg}")
hotel.subscribe("updates", fast_guest)
hotel.subscribe("updates", slow_guest)
# Publishers don't wait for slow subscribers
await hotel.publish("updates", "Pool closing!")
await hotel.publish("updates", "Happy hour!")
await asyncio.sleep(0.2) # Let everything finish
# asyncio.run(demo())
Теперь издатели никогда не блокируют - они просто стоят в очереди и продолжают идти. Фоновый работник обрабатывает доставку всем подписчикам параллельно. Быстрые подписчики быстро получают свои сообщения (0,1s), в то время как медленные тратят свое время (2,5 с), но ни один из них не блокирует другой.
Easy Performance Hack: Uvloop
Асинхронная система Дейва отлично работает, но затем он обнаруживает что-то волшебное: изменение одной строки кода может сделать все в 2-4x быстрее. Дейв считает, что это, очевидно, слишком хорошо, чтобы быть правдой, но все равно решает попробовать.
Входитьuvloop- Замена падения для петли событий Python по умолчанию, которая написана в цинтоне и основанная на Libuv (то же самое, которое делает node.js быстро).
import uvloop
import asyncio
from collections import defaultdict
import time
# The magic line that makes Dave's hotel 2-4x faster
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
class TurboHotelPubSub:
def __init__(self, max_queue_size=10000):
self.channels = defaultdict(list)
self.message_queue = asyncio.Queue(maxsize=max_queue_size)
self.running = True
self.performance_metrics = {
'messages_per_second': 0,
'avg_latency_ms': 0,
'active_subscribers': 0
}
# Start background worker and metrics collector
asyncio.create_task(self._worker())
asyncio.create_task(self._metrics_collector())
def subscribe(self, channel: str, callback):
self.channels[channel].append(callback)
self.performance_metrics['active_subscribers'] = sum(len(subs) for subs in self.channels.values())
print(f"Subscribed to {channel} (Total subscribers: {self.performance_metrics['active_subscribers']})")
async def publish(self, channel: str, message: str):
timestamp = time.time()
message_with_timestamp = (channel, message, timestamp)
try:
self.message_queue.put_nowait(message_with_timestamp)
except asyncio.QueueFull:
# Backpressure - with uvloop, this is much faster
await self.message_queue.put(message_with_timestamp)
async def _worker(self):
"""Background worker - uvloop makes this significantly faster"""
while self.running:
channel, message, publish_time = await self.message_queue.get()
# Measure end-to-end latency
processing_start = time.time()
if channel in self.channels:
# uvloop excels at handling many concurrent tasks
tasks = []
for callback in self.channels[channel]:
if asyncio.iscoroutinefunction(callback):
tasks.append(callback(message))
else:
# uvloop's thread pool is much more efficient
tasks.append(asyncio.get_event_loop().run_in_executor(None, callback, message))
# uvloop's gather is optimized for many concurrent operations
await asyncio.gather(*tasks, return_exceptions=True)
# Track latency
total_latency = (time.time() - publish_time) * 1000 # Convert to ms
self.performance_metrics['avg_latency_ms'] = (
self.performance_metrics['avg_latency_ms'] * 0.9 + total_latency * 0.1
)
self.message_queue.task_done()
async def _metrics_collector(self):
"""Track messages per second - uvloop's timer precision helps here"""
last_time = time.time()
last_count = 0
while self.running:
await asyncio.sleep(1)
current_time = time.time()
# In uvloop, queue.qsize() is more accurate and faster
current_count = getattr(self.message_queue, '_finished', 0)
if current_time - last_time >= 1:
messages_processed = current_count - last_count
self.performance_metrics['messages_per_second'] = messages_processed
last_time, last_count = current_time, current_count
def get_performance_stats(self):
return self.performance_metrics.copy()
# Dave's hotel with uvloop superpowers
async def benchmark_uvloop_vs_standard():
"""Demonstrate uvloop performance improvements"""
# Simulate I/O-heavy subscribers (database writes, API calls)
async def database_subscriber(msg):
# Simulate database write
await asyncio.sleep(0.001) # 1ms "database" call
return f"DB: {msg}"
async def api_subscriber(msg):
# Simulate API call
await asyncio.sleep(0.002) # 2ms "API" call
return f"API: {msg}"
def analytics_subscriber(msg):
# Simulate CPU-heavy sync work
time.sleep(0.0005) # 0.5ms CPU work
return f"Analytics: {msg}"
hotel = TurboHotelPubSub()
# Subscribe multiple handlers to same channel
for i in range(10): # 10 database subscribers
hotel.subscribe("orders", database_subscriber)
for i in range(5): # 5 API subscribers
hotel.subscribe("orders", api_subscriber)
for i in range(20): # 20 analytics subscribers
hotel.subscribe("orders", analytics_subscriber)
print("Starting benchmark with uvloop...")
start_time = time.time()
# Publish lots of messages
for i in range(1000):
await hotel.publish("orders", f"Order #{i}")
# Wait for processing to complete
await hotel.message_queue.join()
end_time = time.time()
stats = hotel.get_performance_stats()
print(f"Benchmark complete in {end_time - start_time:.2f} seconds")
print(f"Performance stats: {stats}")
print(f"Total subscriber callbacks: {stats['active_subscribers'] * 1000:,}")
return end_time - start_time
# Uncomment to run benchmark
# asyncio.run(benchmark_uvloop_vs_standard())
Какие наддушки UVLOOP:
1Подписчики ввода/вывода (2-4x ускорение)
- База данных записывает, вызовы API, операции файлов
- Реализация на основе LIBUP более эффективно обрабатывает тысячи одновременных операций ввода-вывода.
- Отель Дейва теперь может справиться с загрузкой облачного дневника миссис Хендерсон и одновременно посты мистера Петерсона в Instagram
2Многие одновременные подписчики (1,5-2x ускорение)
- Системы с сотнями или тысячами подписчиков на канал
- Оптимизированное планирование задач UVLOOP уменьшает накладные расходы
- Идеально подходит для конференц -центра Дейва с более 500 подписчиками номера
3Операции пула потоков (улучшение 30-50%)
- Синхронизированные обратные вызовы, которые перемещаются в бассейны потоков
- Управление пулом потоков UVLOOP более эффективно
- Лучше для устаревших систем Дейва, которые нельзя сделать асинхронно
4Точность таймера и очереди
- Более точное время для метрик и ограничения ставок
- Лучший мониторинг производительности очереди
- Помогает Дейву отслеживать, не отстает ли его система с спросом
Реальное влияние Uvloop воздействие на отель Дейва:
# Before uvloop: 15,000 notifications/second
# After uvloop: 35,000 notifications/second
# Same code, 2.3x faster!
Самая лучшая часть?Нулевые изменения кодаЗа пределами этой линии. Дейв случайно обнаруживает, что иногда лучшие оптимизации - это те, которые требуют наименьшей работы.
Реакция Дейва: «Подожди, вот и все? Я просто импортирую UVLoop, и все становится быстрее? Это похоже на мошенничество!»
Рассказчик: Это не обман, Дэйв. Это просто хорошая инженерия.
Когда UVLoop больше всего имеет значение:
- Высокие подсчеты подписчиков: 100+ подписчиков на канал
- В/O-тяжелые обратные вызовы: База данных записывает, вызовы API, операции файлов
- Смешанные рабочие нагрузки: Комбинация быстрых и медленных подписчиков
- Чувствительный к задержке: Когда каждый миллисекунд имеет значение
Примечание о трио:В то время как UVLoop делает асинсио быстрее, некоторые разработчики предпочитают трио для сложных систем с тысячами одновременных задач. Структурированная параллелизм Trio и встроенная обработка обратного давления могут быть более надежными при экстремальной нагрузке - она предназначена для изящной, а не загадочно висеть, когда у вас 10 000+ одновременных операций. Для отеля Dave Asyncio+Uvloop идеально подходит. Для следующего предприятия Дейва (торговая система в реальном времени для подводных товаров), трио может предотвратить несколько сеансов отладки в 3 часа ночи.
Установка UVLOOP Gotcha:Дейв пытается установить UVLOOP и запутывается сообщениями об ошибках. Вот в чем дело - UVLOOP требует компиляции, поэтому вам нужны установленные инструменты разработки. На Ubuntu/Debian:apt-get install build-essential
Полем На MacOs с Homebrew:brew install python3-dev
Полем В Windows ... ну, Дейв решает придерживаться стандартного цикла событий в Windows и развернуть в Linux в производстве. Иногда путь наименьшего сопротивления является правильным выбором.
Собирается ядерное: паб/суб
Для экстремальных сценариев производительности или многопроцессов мы можем использовать общую память с полным управлением подписчиками:
import mmap
import struct
import multiprocessing
import threading
import time
from collections import defaultdict
from typing import Callable, Dict, List
class MemoryMappedPubSub:
def __init__(self, buffer_size=1024*1024): # 1MB buffer
# Create shared memory buffer
self.buffer_size = buffer_size
self.shared_file = f'/tmp/pubsub_{multiprocessing.current_process().pid}'
# Initialize memory-mapped file
with open(self.shared_file, 'wb') as f:
f.write(b'\x00' * buffer_size)
self.mmap = mmap.mmap(open(self.shared_file, 'r+b').fileno(), 0)
# Layout: [head_pos][tail_pos][message_data...]
self.head_offset = 0
self.tail_offset = 8
self.data_offset = 16
# Subscriber management
self.subscribers: Dict[str, List[Callable]] = defaultdict(list)
self.listening = False
self.listener_thread = None
def subscribe(self, channel: str, callback: Callable):
"""Subscribe to a channel with a callback function"""
self.subscribers[channel].append(callback)
print(f"Subscribed to channel: {channel}")
# Start listener if not already running
if not self.listening:
self.start_listening()
def start_listening(self):
"""Start background thread to listen for messages"""
if self.listening:
return
self.listening = True
self.listener_thread = threading.Thread(target=self._listen_loop, daemon=True)
self.listener_thread.start()
print("Started listening for messages...")
def stop_listening(self):
"""Stop the message listener"""
self.listening = False
if self.listener_thread:
self.listener_thread.join()
def _listen_loop(self):
"""Background loop that processes incoming messages"""
while self.listening:
messages = self.read_messages()
for message in messages:
self._process_message(message)
time.sleep(0.001) # Small delay to prevent excessive CPU usage
def _process_message(self, message: str):
"""Process a single message and notify subscribers"""
try:
if ':' in message:
channel, content = message.split(':', 1)
if channel in self.subscribers:
for callback in self.subscribers[channel]:
try:
callback(content)
except Exception as e:
print(f"Error in callback for channel {channel}: {e}")
except Exception as e:
print(f"Error processing message: {e}")
def publish(self, channel: str, message: str):
"""Ultra-fast direct memory write"""
data = f"{channel}:{message}".encode()
# Get current tail position
tail = struct.unpack('Q', self.mmap[self.tail_offset:self.tail_offset+8])[0]
# Check if we have enough space (simple wraparound)
available_space = self.buffer_size - self.data_offset - tail
if available_space < len(data) + 4:
# Reset to beginning if we're near the end
tail = 0
# Write message length + data
struct.pack_into('I', self.mmap, self.data_offset + tail, len(data))
self.mmap[self.data_offset + tail + 4:self.data_offset + tail + 4 + len(data)] = data
# Update tail pointer
new_tail = tail + 4 + len(data)
struct.pack_into('Q', self.mmap, self.tail_offset, new_tail)
def read_messages(self):
"""Ultra-fast direct memory read"""
head = struct.unpack('Q', self.mmap[self.head_offset:self.head_offset+8])[0]
tail = struct.unpack('Q', self.mmap[self.tail_offset:self.tail_offset+8])[0]
messages = []
current = head
while current < tail:
try:
# Read message length
msg_len = struct.unpack('I', self.mmap[self.data_offset + current:self.data_offset + current + 4])[0]
# Safety check
if msg_len > self.buffer_size or msg_len <= 0:
break
# Read message data
data = self.mmap[self.data_offset + current + 4:self.data_offset + current + 4 + msg_len]
messages.append(data.decode())
current += 4 + msg_len
except Exception as e:
print(f"Error reading message: {e}")
break
# Update head pointer
struct.pack_into('Q', self.mmap, self.head_offset, current)
return messages
def __del__(self):
"""Cleanup when object is destroyed"""
self.stop_listening()
if hasattr(self, 'mmap'):
self.mmap.close()
# Dave's ultra-fast hotel messaging in action
hotel = MemoryMappedPubSub()
# Define subscriber callbacks
def pool_guest(message):
print(f"🏊 Pool Guest received: {message}")
def restaurant_guest(message):
print(f"🍽️ Restaurant Guest received: {message}")
# Subscribe to channels (automatically starts background listener)
hotel.subscribe("pool", pool_guest)
hotel.subscribe("restaurant", restaurant_guest)
# Publish messages (ultra-fast memory writes)
hotel.publish("pool", "Pool closing in 10 minutes!")
hotel.publish("restaurant", "Happy hour starts now!")
Что делает это особенным:
- Ультрастрабильная публикация: Прямая память пишет, нет системных вызовов
- Автоматическая маршрутизация сообщений: Фоновое потоковое сопровождение сообщений и вызовов подписчиков
- Несколько подписчиков на канал: У каждого канала может быть много слушателей
- Изоляция ошибок: Один плохой обратный вызов не сбивает систему
- Управление чистыми ресурсами: Автоматическая очистка, когда сделано
Версия, отображаемая памятью, дает вам знакомый интерфейс Pub/Sub с производительности корпоративного уровня. Отель Дейва теперь может обрабатывать миллионы гостевых уведомлений в секунду, сохраняя простую модель «подписки и забыть», которая сделала его систему знаменитой.
Сравнение производительности
Решение | Сообщения/второе | Вариант использования | Сложность |
---|---|---|---|
Основной паб/sub | ~ 50K | Небольшие приложения, прототипы | ⭐ просто |
В очереди паб/sub | ~ 200K | Большинство производственных систем | ⭐⭐ Умеренный |
Память | ~ 5M+ | Высокочастотная торговля, многопроцесс | ⭐⭐⭐⭐⭐ Эксперт |
Внешний (Redis) | ~ 100K+ | Распределенные системы | ⭐⭐⭐ Умеренный |
Когда использовать каждый:
- Базовый: Обучение, небольшие проекты, <10K сообщений/сек.
- В очереди: Большинство реальных приложений, хорошо обрабатывают всплески трафика
- Память:> 500K Messages/Sec, Cross-Brocess Communication, Ultra-Low Latency
- Внешний: Несколько серверов, настойчивость, проверенная надежность
Добавьте обработку ошибок, настойчивость и масштабирование, и у вас есть обмен сообщениями предприятия.
Популярный паб/суб -технологии
- Redis Pub/Sub: Просто, быстро, отлично подходит для функций в реальном времени
- Апач Кафка: Enterprise Grade, обрабатывает массивную пропускную способность
- Rabbitmq: Надежная очередь сообщений с гибкой маршрутизацией
- Облачные решения: AWS SNS/SQS, Google Pub/Sub, Azure Service Bus
Конец истории Дейва
Шесть месяцев спустя Дэйв повышается до «главного сотрудника по инновациям в коммуникации», потому что результаты удовлетворенности гостей находятся через крышу. Отель теперь известен своей «революционной системой обмена сообщениями».
Дейв по -прежнему думает, что «каналы» относится к системе кабельного телевидения отеля, но его зоны интермодов обучают студентов по информатике во всем мире, как работают модели обмена сообщениями.
Мораль истории? Даже остановленные часы правы два раза в день, и даже Дейв может наткнуться на хорошую архитектуру, если он сначала сломает достаточно вещей.
А как насчет Дэйва?Он вернулся к тому, что знает лучше всего - транспортные системы. Его новое предприятие - революционизация подводного общественного транспорта с «государственными подводными лодками», которые работают на «автобусах данных».
Когда его спрашивают о технической архитектуре, Дейв с энтузиазмом объясняет: «Каждый подкапливает свое местоположение пассажирам, которые подписываются на конкретные маршруты. Это паб/суб, но для подводных лодок! И вместо регулярных двигателей они работают на шинах данных - вы знаете, для максимальной пропускной способности!»
Городской транспортный департамент все еще пытается выяснить, является ли он гением или им нужно отозвать его бизнес -лицензию. Дэйв остается убежденным, что решает «проблему последней мили для водных пассажиров».
Дэйв был настолько воодушевлен успехом своей новой системы, что он разветвлялся и начал новый бизнес. В следующий раз, когда вы будете в своей местной водопое, вы можете увидеть новую "паб -саббат", в котором участвуют революционные «асинхронно обработанные асинхронные сэндвичи для ремесленных подводных лодок с неблокирующими очередями ингредиентов». Мясо сидит в теплом рассоле для «оптимальной настойчивости посланий», а Дейв настаивает на том, что сырой хлеб на самом деле «укрепляется влагой для лучшей пропускной способности».
Он все еще получает рождественские открытки от гостей отеля.
Оригинал