В байтах (C++ layout): | Смещение | Размер | Тип данных | Описание | | :------- | :----- | :----------------------------------- | :-------------------------------------------------------- | | 0 | 1 | `uint8_t` (atomic) | `ready` (0 = не готова, 1 = готова) | | 1 | 1 | `uint8_t` | `op` — код операции (`PUT` = 0, `DELETE` = 1, `FIND` = 2) | | 2 | 16 | `uint8_t[16]` | `key` — хэш ключа (Hash128, 128 бит) | | 18 | 4 | `uint32_t` | `value_size` — длина данных `value` | | 22 | 1024 | `uint8_t[1024]` | `value` — содержимое для записи (PUT) | | 1046 | 4 | `uint32_t` | `response_size` — длина ответа в `response` | | 1050 | 1024 | `uint8_t[1024]` | `response` — содержимое ответа после FIND | | 2074 | 1 | `uint8_t` (atomic) | `response_ready` (0 = нет ответа, 1 = ответ готов) | | --- | --- | Итого: **2075 байт** на одну команду | | > ⚠️ Реально в памяти компилятор **добавит выравнивание**.\ > Обычно команда занимает **2080 байт** с учетом паддинга (например, до 8 байт). --- ## 2. Структура очереди `SharedCommandQueue` | Смещение | Размер | Тип данных | Описание | | :------- | :------- | :---------------- | :----------------------------- | | 0 | 8 | `size_t` (atomic) | `head` (где следующий push) | | 8 | 8 | `size_t` (atomic) | `tail` (где следующий pop) | | 16 | 8 | `size_t` | `capacity` (количество слотов) | | 24 | 2080 × N | `Command[N]` | Массив команд | > **N** = максимальное количество команд в очереди. Например: 64. --- # 📦 Процесс записи команды (PUT, FIND, DELETE) 1. Клиент читает `head`. 2. Вычисляет `next_head = (head + 1) % capacity`. 3. Проверяет, что `next_head != tail` (иначе очередь заполнена, нужно ждать). 4. Пишет данные в слот `Command` по индексу `head`: - ставит `ready = 0` - заполняет `op`, `key`, `value_size`, `value` - обнуляет `response_size`, `response_ready` 5. После полной записи устанавливает `ready = 1` (memory\_order\_release). 6. После этого обновляет `head = next_head` (memory\_order\_release). --- # 🧩 Процесс чтения команды сервером 1. Сервер читает `tail`. 2. Если `tail == head`, значит нет команд (ждет). 3. Читает слот `Command` по индексу `tail`. 4. Проверяет `ready == 1`. 5. Выполняет команду: - для `PUT`, `DELETE` ничего не нужно возвращать - для `FIND` после обработки: - заполняет `response` - устанавливает `response_size` - устанавливает `response_ready = 1` (memory\_order\_release) 6. После обработки увеличивает `tail = (tail + 1) % capacity` (memory\_order\_release). --- # 📑 Коды операций | Код | Операция | | :-- | :------- | | 0 | PUT | | 1 | DELETE | | 2 | FIND | --- # 📈 Диаграмма ``` Client (head) Server (tail) ↓ ↑ [ empty ][ empty ][ empty ][ empty ][ empty ] ↑ write PUT -> ready=1 ↑ read -> process -> tail++ ↑ write FIND -> ready=1 ↑ read -> process -> response_ready=1 ``` --- # 📋 Минимальная инструкция для других языков 1. Открыть или создать POSIX shared memory (`/shm_rates1`). 2. Маппить размер (например: 24 + 2080×64 байта = \~133 120 байт). 3. Знать layout структуры `SharedCommandQueue` и `Command`. 4. Атомарно обновлять `head`/`tail` через обычные load/store (x86/arm архитектуры обеспечивают это через aligned операции на 64 бита). --- # ⚙️ Примерная формула расчета памяти: ```text total_size = 24 + (round_up(sizeof(Command)) × capacity) ``` - **24 байта** на `head` + `tail` + `capacity` - **capacity** = число команд - **sizeof(Command)** = около 2080 байт --- # ❗ Важно - `head` и `tail` — должны быть атомарными. - Доступ к слотам только по индексам `(head % capacity)` или `(tail % capacity)`. - Все указанные поля фиксированы и известны по смещению. - Все строки (`value`, `response`) — это просто байтовые массивы без `\0`. --- # 📦 Памятка: Структура SharedCommandQueue в памяти ### Общая структура: | Смещение (байты) | Размер | Описание | | :--------------- | :------- | :--------------------------------- | | 0 | 8 | `head` (size\_t, atomic) | | 8 | 8 | `tail` (size\_t, atomic) | | 16 | 8 | `capacity` (size\_t, НЕ atomic) | | 24 | 2080 × N | Команды Command\[] (N штук подряд) | --- # 📦 Памятка: Структура одного `Command` | Смещение | Размер | Описание | | :-------- | :--------- | :----------------------------------- | | 0 | 1 byte | `ready` (atomic\) | | 1 | 1 byte | `op` (uint8\_t) | | 2 | 16 bytes | `key` (Hash128, 128 бит) | | 18 | 4 bytes | `value_size` (uint32\_t) | | 22 | 1024 bytes | `value` (байтовый массив данных) | | 1046 | 4 bytes | `response_size` (uint32\_t) | | 1050 | 1024 bytes | `response` (ответ от сервера) | | 2074 | 1 byte | `response_ready` (atomic\) | | 2075–2079 | 5 bytes | padding (выравнивание до 8 байт) | > ⚡ Суммарный размер одной команды с паддингом = **2080 байт**. --- # 📋 HEX Layout всего буфера (SharedCommandQueue + команды) ``` 00:00 - 00:07 -> head (size_t, LE) 00:08 - 00:0F -> tail (size_t, LE) 00:10 - 00:17 -> capacity (size_t, LE) 00:18 - 00:18+2080*N -> N команд, каждая: 00:00 -> ready (1 byte) 00:01 -> op (1 byte) 00:02 - 00:11 -> key (16 bytes) 00:12 - 00:15 -> value_size (4 bytes, LE) 00:16 - 04:15 -> value (1024 bytes) 04:16 - 04:19 -> response_size (4 bytes, LE) 04:20 - 08:1F -> response (1024 bytes) 08:20 -> response_ready (1 byte) 08:21 - 08:27 -> padding (5 bytes) ``` --- # 🧪 Мини-псевдокод для любого языка ```pseudo ptr = mmap(...) head = read_uint64(ptr, 0) tail = read_uint64(ptr, 8) capacity = read_uint64(ptr, 16) command_base = 24 // Пример: прочитать первую команду (index = 0) offset = command_base + index * 2080 ready = read_uint8(ptr, offset + 0) op = read_uint8(ptr, offset + 1) key = read_bytes(ptr, offset + 2, 16) value_size = read_uint32(ptr, offset + 18) value = read_bytes(ptr, offset + 22, value_size) response_ready = read_uint8(ptr, offset + 2074) response_size = read_uint32(ptr, offset + 1046) response = read_bytes(ptr, offset + 1050, response_size) ``` --- # 📊 Краткая таблица памяти: | Поле | Тип | Офсет | Размер | | :------- | :----------- | :---- | :----- | | head | size\_t (8B) | 0 | 8B | | tail | size\_t (8B) | 8 | 8B | | capacity | size\_t (8B) | 16 | 8B | | commands | Command\[N] | 24 | 2080×N | --- # 📢 Важные правила при использовании - Всегда выравнивание по 8 байтам (иначе UB на ARM/M1). - Использовать little-endian (x86, arm64 всегда LE). - При копировании строк следить за value\_size / response\_size. - Проверять флаг `ready` перед чтением команды. - После обработки команды — инкрементировать `tail` мод capacity. - Ответ через `response` + `response_size` + `response_ready` только для `FIND`. --- # Конфиг Пример: ```toml # Список баз данных [[database]] name = "rates1" [[database]] name = "test" # Настройки базы rates1 [databases.rates1] shm_queue_capacity = 2048 # Размер очереди команд в shared memory (количество команд) max_memtable_size = 2097152 # Максимальный размер активной Memtable в байтах (до сброса на диск) l0_queue_capacity = 2048 # Размер очереди для фоновой компакции L0 SSTables estimated_element_size = 256 # Оценка среднего размера одного элемента в Memtable в байтах # Настройки базы test [databases.test] shm_queue_capacity = 1024 max_memtable_size = 1048576 l0_queue_capacity = 1024 estimated_element_size = 128 ``` ## Замечания: Если параметры не заданы для конкретной базы, используются значения по умолчанию: ```cpp shm_queue_capacity = 1024 max_memtable_size = 1048576 l0_queue_capacity = 1024 estimated_element_size = 128 ``` > Все размеры (max_memtable_size, estimated_element_size) указываются в байтах. > Shared Memory для каждой базы будет создана с именем /shm_<имя_базы>. > Очереди (l0_queue, shm_queue) построены на lock-free кольцевых буферах. ## Полное описание параметров конфига | Параметр | Тип | Описание | | :----------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `shm_queue_capacity` | `size_t` | **Размер очереди команд в shared memory**.
Это количество слотов команд (PUT/DELETE/FIND), которые могут одновременно находиться в памяти. Если очередь заполнится — новые команды временно не смогут быть отправлены.
**Важно**: shared memory выделяется исходя из этого размера, поэтому очередь большого размера требует больше памяти. | | `max_memtable_size` | `size_t` | **Максимальный размер активной Memtable в байтах**.
Когда объем данных в Memtable превышает это значение, Memtable сбрасывается на диск в SSTable-файл.
**Если значение маленькое** — будет много мелких SSTable-файлов.
**Если значение большое** — реже будет происходить сброс, но потребление оперативной памяти будет выше. | | `l0_queue_capacity` | `size_t` | **Размер очереди файлов для компакции L0 уровня**.
Когда Memtable сбрасывается в SSTable, файл ставится в очередь на компакцию. Этот параметр задает максимальное количество файлов, которые можно держать в очереди одновременно.
**Если значение маленькое** — компакция будет происходить чаще.
**Если большое** — будет задержка в обработке компакции при большом потоке вставок. | | `estimated_element_size` | `size_t` | **Оценка среднего размера одного элемента в Memtable**.
Используется для **приближенного** расчета реального размера Memtable. Нужно, чтобы **не замерять точно каждый вставленный элемент**, а быстро определять когда пора сбрасывать Memtable.
**Типичный случай**: текстовые значения без больших вложений имеют размер 64-256 байт. |