Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Local scratch / backup area (never committed)
.backup/

# Build directories
build/
build-*/
Expand Down
33 changes: 32 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.5.3] - 2026-04-12

### Added

- **Reactor I/O model (epoll/kqueue)** — New event-driven TCP path replaces the blocking one-thread-per-connection loop; a single event-loop thread plus a bounded worker pool serves thousands of persistent connections
- **Per-connection slow-reader backpressure** — `api.tcp.max_write_queue_bytes` (default 16 MiB) force-closes clients whose enqueued response bytes exceed the cap
- **Reactor error codes** — `kNetworkReactorUnsupported`/`PollFailed`/`RegisterFailed`/`ModifyFailed`/`RemoveFailed`/`QueueFull`/`AlreadyOpen` (6016–6023)

### Fixed

- **TCP half-close drain regression** — `shutdown(SHUT_WR)` + `recv()` clients now receive their response; `kHangup` events no longer short-circuit to `OnError`, and `read_eof_` is tracked separately from `closing_` so the drain task can enqueue the response
- **Rate limiting silently disabled under reactor** — `api.rate_limiting.enable = true` is now honored on every accepted connection; the reactor handler calls `getpeername()` + `AllowRequest()` before `Register()` and returns `SERVER_BUSY` on rejection
- **Unix domain socket acceptor could not start** — Removed the dead secondary `unix_acceptor_` that collided with the primary acceptor's own UDS bind; UDS now flows end-to-end through the primary acceptor's reactor handler
- **Grafana memory usage PromQL** — Use `ignoring(type)` on the division so `mygramdb_memory_used_bytes{type="total"}` matches the denominator label set

### Changed

- **Blocking I/O path removed entirely** — `ConnectionIOHandler`, `TcpServer::HandleConnection`, the `api.tcp.io_model` feature flag, `connection_contexts_` map, `ConnectionAcceptor::SetConnectionHandler`, and the `BlockingMode` ctest entries are all deleted
- **Thread-pool auto-size floor reverted** — Dropped the emergency `hw*4`/64-worker mitigation for blocking-mode starvation; restored `max(hw*2, 4)`
- **Reactor hot-path polish** — epoll/kqueue poll buffers grow on demand up to 4 KiB entries; `Register`/`Stop` race closed by re-checking `running_` under `mux_lifecycle_` shared; `OnWritable` empty-queue teardown flattened

### Testing

- New unit tests: `event_multiplexer_test`, `io_reactor_test`, `reactor_connection_test`
- New integration tests: `reactor_integration_test` (write backpressure, many-idle-connections, half-close, rate limit, UDS, max query length), `reactor_starvation_regression_test`, `thread_pool_saturation_test` (migrated, assertion inverted for reactor default)
- e2e `test_half_close_write` now passes (previously failing on reactor path)

**Detailed Release Notes**: [docs/releases/v1.5.3.md](docs/releases/v1.5.3.md)

## [1.5.2] - 2026-04-09

### Added
Expand Down Expand Up @@ -444,7 +473,9 @@ Initial release with core search engine functionality and MySQL replication supp

---

[Unreleased]: https://github.com/libraz/mygram-db/compare/v1.5.1...HEAD
[Unreleased]: https://github.com/libraz/mygram-db/compare/v1.5.3...HEAD
[1.5.3]: https://github.com/libraz/mygram-db/compare/v1.5.2...v1.5.3
[1.5.2]: https://github.com/libraz/mygram-db/compare/v1.5.1...v1.5.2
[1.5.1]: https://github.com/libraz/mygram-db/compare/v1.5.0...v1.5.1
[1.5.0]: https://github.com/libraz/mygram-db/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/libraz/mygram-db/compare/v1.3.9...v1.4.0
Expand Down
44 changes: 28 additions & 16 deletions docs/en/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,19 +300,27 @@ graph TB
#### Request Dispatch Pipeline

**ConnectionAcceptor** (`connection_acceptor.h`)
- **Responsibility**: Socket accept loop
- **Responsibility**: Socket accept loop (TCP or UDS)
- **Features**:
- `SO_RCVTIMEO` to prevent indefinite hangs
- Dispatches connections to thread pool
- Thread-safe connection tracking
- Hands off each accepted fd to `IoReactor` inline (accept thread → reactor, no thread-pool hop)
- max_connections gate with SERVER_BUSY backpressure
- Thread-safe `active_fds_` tracking

**ConnectionIOHandler** (`connection_io_handler.h`)
- **Responsibility**: Per-connection I/O handling
**IoReactor** (`io_reactor.h`)
- **Responsibility**: Single-threaded event loop for readiness notification
- **Features**:
- Reads/buffers socket data
- Parses protocol messages (delimited by `\r\n`)
- Enforces maximum query length (default 1MB)
- Writes responses to socket
- `EventMultiplexer` abstraction (Linux: epoll, macOS/BSD: kqueue)
- Per-fd `ReactorConnection` lifecycle management
- Write arm/disarm (EPOLLOUT / EVFILT_WRITE), close callback
- Graceful shutdown

**ReactorConnection** (`reactor_connection.h`)
- **Responsibility**: Per-connection I/O state and drain-task pattern
- **Features**:
- Non-blocking `recv()` drains into `read_buf_`, frames on `\r\n`
- Schedules at most one in-flight drain task per connection on the worker pool ("clear-then-recheck" reschedule)
- Non-blocking write queue (inline fast path + EPOLLOUT fallback on EAGAIN)
- Per-connection `max_write_queue_bytes` slow-reader backpressure cap

**RequestDispatcher** (`request_dispatcher.h`)
- **Responsibility**: Application logic routing
Expand Down Expand Up @@ -565,15 +573,17 @@ graph TB
MainThread["Main Thread<br/>Application::RunMainLoop()<br/>Signal polling (SignalManager)<br/>Config reload handling<br/>Initialization/shutdown coordination"]
BinlogThread["BinlogReader Thread (if MySQL enabled)<br/>Reads from MySQL binlog, queues events"]
EventLoop["Event Processing Loop (main thread)<br/>Dequeues binlog events, applies to Index/DocumentStore"]
TCPThread["TCP Server Accept Thread<br/>Listens on TCP port, accepts connections"]
TCPThread["TCP/UDS Accept Thread<br/>Listens on socket, hands accepted fds directly to IoReactor"]
ReactorThread["IoReactor Event Loop Thread (single)<br/>Drains epoll_wait/kevent, dispatches readiness to ReactorConnection<br/>Owns per-fd write arm/disarm and close"]
HTTPThread["HTTP Server Thread<br/>Listens on HTTP port"]
WorkerPool["Worker Thread Pool (configurable, default = CPU count)<br/>Thread 1: Processes client requests from queue<br/>Thread 2: ...<br/>Thread N: ..."]
WorkerPool["Worker Thread Pool (configurable, default = CPU count)<br/>Thread 1: Runs per-connection drain tasks (request processing + inline send)<br/>Thread 2: ...<br/>Thread N: ..."]
SnapshotThread["SnapshotScheduler Background Thread (if enabled)<br/>Periodically creates snapshots"]

Main --> MainThread
Main --> BinlogThread
Main --> EventLoop
Main --> TCPThread
Main --> ReactorThread
Main --> HTTPThread
Main --> WorkerPool
Main --> SnapshotThread
Expand Down Expand Up @@ -719,8 +729,9 @@ Following the dependency graph, components are initialized in this order:
- AdminHandler, ReplicationHandler, DebugHandler
- CacheHandler, SyncHandler (MySQL)
6. **RequestDispatcher** (depends on handlers)
7. **ConnectionAcceptor** (depends on thread pool)
8. **SnapshotScheduler** (optional, depends on catalog)
7. **ConnectionAcceptor** (depends only on ServerConfig; the reactor handler is installed later in `TcpServer::Start()`)
8. **IoReactor** (created in `TcpServer::Start()`, depends on ThreadPool and RequestDispatcher)
9. **SnapshotScheduler** (optional, depends on catalog)

**Note**: RateLimiter and SyncOperationManager are created in `TcpServer::Start()` before ServerLifecycleManager is instantiated.

Expand Down Expand Up @@ -932,8 +943,9 @@ class InvalidationManager {

### Between Acceptor and Handlers

- **ConnectionAcceptor** → accepts connection → submits to **ThreadPool**
- **ThreadPool worker** → calls **ConnectionIOHandler** → calls **RequestDispatcher** → calls handler
- **ConnectionAcceptor** → accepts connection → hands the fd directly to **IoReactor::Register** (no thread-pool hop)
- **IoReactor (event loop thread)** → detects readable → **ReactorConnection::OnReadable** → extracts frames → schedules a drain task on **ThreadPool**
- **ThreadPool worker** → **ReactorConnection::DrainTask** → dispatches each frame via **RequestDispatcher** → handler → response sent through `EnqueueResponse()` (inline fast path; partial sends fall back to EPOLLOUT in the event loop)

---

Expand Down
44 changes: 28 additions & 16 deletions docs/ja/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,19 +300,27 @@ graph TB
#### リクエストディスパッチパイプライン

**ConnectionAcceptor** (`connection_acceptor.h`)
- **責務**: ソケット受け入れループ
- **責務**: ソケット受け入れループ (TCP または UDS)
- **機能**:
- 無期限ハングを防ぐ`SO_RCVTIMEO`
- スレッドプールへの接続ディスパッチ
- スレッドセーフな接続トラッキング
- `accept()` した fd を `IoReactor` にインラインで引き渡す(accept スレッド → reactor、ワーカープールを経由しない)
- max_connections のゲート、SERVER_BUSY バックプレッシャー
- スレッドセーフな active_fds_ トラッキング

**ConnectionIOHandler** (`connection_io_handler.h`)
- **責務**: 接続ごとのI/Oハンドリング
**IoReactor** (`io_reactor.h`)
- **責務**: 単一スレッドのイベントループでの readiness 通知
- **機能**:
- ソケットデータの読み取り/バッファリング
- プロトコルメッセージの解析(`\r\n`で区切り)
- 最大クエリ長の強制(デフォルト1MB)
- ソケットへのレスポンス書き込み
- `EventMultiplexer` 抽象(Linux: epoll、macOS/BSD: kqueue)
- per-fd `ReactorConnection` のライフサイクル管理
- 書き込みの arm/disarm (EPOLLOUT / EVFILT_WRITE)、close コールバック
- graceful shutdown

**ReactorConnection** (`reactor_connection.h`)
- **責務**: 接続ごとの I/O 状態と drain タスクパターン
- **機能**:
- 非ブロッキング `recv()` で `read_buf_` にドレイン、`\r\n` 区切りでフレーム化
- drain task をワーカープールに 1 本だけ in-flight でスケジュール("clear-then-recheck" 再スケジュール)
- 非ブロッキング書き込みキュー(インライン高速パス + EAGAIN 時は EPOLLOUT フォールバック)
- per-connection `max_write_queue_bytes` でスローリーダーバックプレッシャー

**RequestDispatcher** (`request_dispatcher.h`)
- **責務**: アプリケーションロジックルーティング
Expand Down Expand Up @@ -565,15 +573,17 @@ graph TB
MainThread["Main Thread<br/>Application::RunMainLoop()<br/>シグナルポーリング (SignalManager)<br/>設定リロード処理<br/>初期化/シャットダウン調整"]
BinlogThread["BinlogReader Thread (if MySQL enabled)<br/>MySQLバイナリログを読み取り、イベントをキューイング"]
EventLoop["Event Processing Loop (main thread)<br/>バイナリログイベントをデキュー、Index/DocumentStoreに適用"]
TCPThread["TCP Server Accept Thread<br/>TCPポートでリスン、接続を受け入れ"]
TCPThread["TCP/UDS Accept Thread<br/>ソケットでリスン、accept した fd を IoReactor に直接登録"]
ReactorThread["IoReactor Event Loop Thread (1 本)<br/>epoll_wait/kevent でドレイン、ReactorConnection に readiness を配送<br/>per-fd の write arm/disarm と close を担当"]
HTTPThread["HTTP Server Thread<br/>HTTPポートでリスン"]
WorkerPool["Worker Thread Pool (configurable, default = CPU count)<br/>Thread 1: キューからクライアントリクエストを処理<br/>Thread 2: ...<br/>Thread N: ..."]
WorkerPool["Worker Thread Pool (configurable, default = CPU count)<br/>Thread 1: per-connection drain task を実行(リクエスト処理 + 高速送信)<br/>Thread 2: ...<br/>Thread N: ..."]
SnapshotThread["SnapshotScheduler Background Thread (if enabled)<br/>定期的にスナップショットを作成"]

Main --> MainThread
Main --> BinlogThread
Main --> EventLoop
Main --> TCPThread
Main --> ReactorThread
Main --> HTTPThread
Main --> WorkerPool
Main --> SnapshotThread
Expand Down Expand Up @@ -720,8 +730,9 @@ graph TB
- AdminHandler、ReplicationHandler、DebugHandler
- CacheHandler、SyncHandler(MySQL)
6. **RequestDispatcher**(ハンドラーに依存)
7. **ConnectionAcceptor**(ThreadPoolに依存)
8. **SnapshotScheduler**(オプショナル、catalogに依存)
7. **ConnectionAcceptor**(ServerConfig のみに依存。reactor handler は `TcpServer::Start()` で後付け)
8. **IoReactor**(`TcpServer::Start()` で生成、ThreadPool と RequestDispatcher に依存)
9. **SnapshotScheduler**(オプショナル、catalogに依存)

**注記**: RateLimiterとSyncOperationManagerは、ServerLifecycleManagerがインスタンス化される前に`TcpServer::Start()`で作成されます。

Expand Down Expand Up @@ -933,8 +944,9 @@ class InvalidationManager {

### AcceptorとHandlersの間

- **ConnectionAcceptor** → 接続を受け入れ → **ThreadPool**に投入
- **ThreadPoolワーカー** → **ConnectionIOHandler**を呼び出し → **RequestDispatcher**を呼び出し → ハンドラを呼び出し
- **ConnectionAcceptor** → 接続を受け入れ → **IoReactor::Register** に直接渡す(thread pool ホップなし)
- **IoReactor (event loop thread)** → 読み取り可能を検知 → **ReactorConnection::OnReadable** → フレームを抽出 → drain task を **ThreadPool** にスケジュール
- **ThreadPool ワーカー** → **ReactorConnection::DrainTask** → **RequestDispatcher** にフレームをディスパッチ → ハンドラ → レスポンスを `EnqueueResponse()` → 高速パスでインライン送信、不完全送信時は EPOLLOUT で event loop に再送出

---

Expand Down
3 changes: 2 additions & 1 deletion docs/releases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ This directory contains detailed release notes for each version of MygramDB.

## Available Versions

- [v1.5.2](v1.5.2.md) - Latest release (2026-04-09) - MySQL 9.x compatibility, VECTOR type support, Auth fix
- [v1.5.3](v1.5.3.md) - Latest release (2026-04-12) - Reactor I/O Model (epoll/kqueue), Half-close Fix, Rate Limit & UDS Hardening
- [v1.5.2](v1.5.2.md) - 2026-04-09 - MySQL 9.x compatibility, VECTOR type support, Auth fix
- [v1.5.1](v1.5.1.md) - 2026-04-01 - Multi-distro packaging (EL10, Ubuntu DEB), Package verification, Build fixes
- [v1.5.0](v1.5.0.md) - 2026-03-23 - verify_text Post-Filter, Docker Benchmark, Namespace Cleanup
- [v1.4.0](v1.4.0.md) - 2026-03-16 - Unix Socket, Prometheus Metrics, Benchmark Suite & Bug Fixes
Expand Down
Loading
Loading