Sprint Backlog 06
+tags: { code(c) infra(i) analysis(n) agile(a) }
Sprint Mission
- finish up all remaining tasks around domain entities.
- templatise domain entity generation.
Stories
Active
| Tags | Headline | Time | % | ||
|---|---|---|---|---|---|
| Total time | 5:57 | 100.0 | |||
| Stories | 5:57 | 100.0 | |||
| Active | 5:57 | 100.0 | |||
| infra | OCR scan notebooks for this sprint | 0:33 | 9.2 | ||
| code | Add a version attribute to entities | 0:05 | 1.4 | ||
| code | Retest all recipes after latest changes | 0:58 | 16.2 | ||
| code | Server must check database connectivity on startup | 1:50 | 30.8 | ||
| code | Add system tray support | 0:53 | 14.8 | ||
| code | Red build after account lock PR | 0:10 | 2.8 | ||
| code | Valgrind is detecting issues with comms tests | 0:09 | 2.5 | ||
| code | Saving a new currency should close window | 0:15 | 4.2 | ||
| code | Consider adding top-level login and logout commands in shell | 0:28 | 7.8 | ||
| code | Retry logic does not notify give-up | 0:36 | 10.1 |
| Tags | Headline | Time | % | ||
|---|---|---|---|---|---|
| Total time | 77:53 | 100.0 | |||
| Stories | 77:53 | 100.0 | |||
| Active | 77:53 | 100.0 | |||
| agile | Sprint and product backlog refinement | 0:29 | 0.6 | ||
| infra | OCR scan notebooks for this sprint | 16:15 | 20.9 | ||
| code | Implement session cancellation | 2:42 | 3.5 | ||
| code | Create a variability service for feature flags | 1:23 | 1.8 | ||
| code | Add tests to diagrams | 1:26 | 1.8 | ||
| code | Add messaging to variability | 0:40 | 0.9 | ||
| code | Create handshake service in comms | 2:31 | 3.2 | ||
| code | Implement client heartbeat for disconnect detection | 5:18 | 6.8 | ||
| code | Pressing disconnect crashes client | 0:23 | 0.5 | ||
| infra | Create a component creator skill | 0:42 | 0.9 | ||
| code | Add listen/notify support | 11:25 | 14.7 | ||
| code | Move context to database | 0:20 | 0.4 | ||
| code | Use std::string_view for loggers |
3:14 | 4.2 | ||
| code | Fix issues with logging after string_view change | 0:53 | 1.1 | ||
| code | Remove bootstrap mode from context | 6:36 | 8.5 | ||
| code | Add tests for messaging handler | 0:14 | 0.3 | ||
| code | Add retry algorithm to client | 1:52 | 2.4 | ||
| code | Fix valgrind leaks after string view changes to logging | 0:07 | 0.1 | ||
| code | Remove enum exception | 0:20 | 0.4 | ||
| code | Entity syntax refactor snags | 0:40 | 0.9 | ||
| code | Experiment with simple code generation | 1:30 | 1.9 | ||
| code | Add postgres listener | 1:59 | 2.5 | ||
| code | Add a logout message | 0:22 | 0.5 | ||
| code | Implement the event bus | 2:33 | 3.3 | ||
| code | Add a version attribute to entities | 5:59 | 7.7 | ||
| code | Retest all recipes after latest changes | 3:39 | 4.7 | ||
| code | Server must check database connectivity on startup | 1:50 | 2.4 | ||
| code | Add system tray support | 0:53 | 1.1 | ||
| code | Red build after account lock PR | 0:10 | 0.2 | ||
| code | Valgrind is detecting issues with comms tests | 0:09 | 0.2 | ||
| code | Saving a new currency should close window | 0:15 | 0.3 | ||
| code | Consider adding top-level login and logout commands in shell | 0:28 | 0.6 | ||
| code | Retry logic does not notify give-up | 0:36 | 0.8 |
COMPLETED Sprint and product backlog refinement agile
Updates to sprint and product backlog.
COMPLETED Add AI generated sprint summary infra
At the end of the sprint, generate the sprint summary using the prompt.
COMPLETED OCR scan notebooks for this sprint infra
We need to scan all of our finance notebooks so we can use them with AI. Each sprint will have a story similar to this until we scan and process them all.
COMPLETED Implement session cancellation code
DEADLINE:
When the server stops (via SIGINT or programmatic shutdown), all active sessions currently continue running indefinitely, preventing clean shutdown. The server needs to gracefully cancel all ongoing session I/O operations without waiting indefinitely.
This is critical for production deployment where clean shutdown is required for container orchestration, service restarts, and graceful degradation scenarios.
Acceptance criteria:
- Server can stop cleanly with multiple active sessions running
- All session I/O operations are cancelled when server::stop() is called
- No indefinite hangs during shutdown process
- Sessions log their cancellation for observability
- Existing cancellation architecture (cancellation_signal) is preserved
- Solution uses Asio's hierarchical cancellation model
- Tasks
[X]Add root cancellation signal for sessions in server class[X]Expose cancellation slot method in server (get_session_cancellation_slot)[X]Pass cancellation slot to each new session in accept_loop[X]Update session constructor to accept and store cancellation slot[X]Bind all session I/O operations to the cancellation slot[X]Update server::stop() to emit session cancellation signal[X]Add session cleanup logging for operation_aborted cases[X]Write integration test with multiple concurrent sessions[X]Test shutdown behavior with Ctrl+C signal[X]Verify no resource leaks during cancellation[X]Address Gemini code review: remove redundant signal handler[X]Address Gemini code review: explicitly close connection on read failure[X]Address Gemini code review: enhance test to verify client communication fails
Notes
Original implementation approach: Use hierarchical cancellation model where server owns a root cancellation signal and each session inherits a slot from this root signal. When server stops, it emits cancellation which propagates to all sessions automatically.
Actual implementation: After investigation, we discovered a critical limitation in Boost.Asio's cancellation API: a cancellation_signal contains exactly ONE slot that holds ONE handler at a time. Multiple sessions calling slot.assign() overwrite each other's handlers, causing only the last session to be cancelled.
This was confirmed by:
- Testing showed only 1/3 sessions cancelled
- Deepseek "idiomatic" example segfaulted immediately
- Official Boost.Asio documentation clarified the one-slot-per-signal limitation
Final solution: Explicit session management
- Server maintains std::list<std::shared_ptr<session>> of active sessions
- Mutex protects concurrent access to the sessions list
- Sessions added to list when created, removed when complete
- server::stop() explicitly calls session::stop() on each active session
- session::stop() closes the connection, causing pending I/O to fail
Benefits of final approach:
- Correct and reliable (no API misuse)
- Clear ownership model
- Thread-safe with mutex protection
- All 3/3 sessions properly cancelled in tests
Files modified:
projects/ores.comms/include/ores.comms/server.hpp- Add active_sessions_ listprojects/ores.comms/src/server.cpp- Update accept_loop and stop()projects/ores.comms/include/ores.comms/session.hpp- Add stop() methodprojects/ores.comms/src/session.cpp- Implement stop(), remove slot parametersprojects/ores.service/src/main.cpp- Remove redundant signal handlerprojects/ores.comms/src/net/client.cpp- Add explicit close on read failureprojects/ores.comms/tests/net_client_tests.cpp- Enhanced verification test
- Code review from Gemini
- COMPLETED projects/ores.service/src/main.cpp
- priority: high
- status: FIXED
- Comment on lines +61 to +69:
// Install signal handlers to allow graceful shutdown // This prevents the default SIGINT/SIGTERM handlers from terminating // the process immediately, giving the server time to clean up boost::asio::signal_set signals(io_ctx, SIGINT, SIGTERM); signals.async_wait([&io_ctx](const boost::system::error_code&, int signal) { std::cout << "\nReceived signal " << signal << ", initiating shutdown..." << std::endl; // Don't stop io_context here - let the server's signal handler do the cleanup // The server will stop the io_context when it's done });
This new signal handler in main creates a race condition with the server's internal signal handler (server::watch_for_stop_signals). Both handlers wait for SIGINT/SIGTERM on the same io_context, but it's undefined which one will be invoked. If this handler in main is triggered, it only prints a message and does not initiate the server shutdown, which would prevent the graceful shutdown from occurring.
The server's own signal handler is sufficient to both prevent immediate process termination and trigger the graceful shutdown by calling server::stop(). Relying on the server's handler alone will make the shutdown process more reliable.
Additionally, the comment on line 68 is slightly misleading. The server's stop() method doesn't stop the io_context; it's stopped on line 75 after async_main completes, which is the correct behavior.
- COMPLETED projects/ores.comms/src/net/client.cpp
- priority: medium
- status: FIXED
- Comment on lines +259 to +263
{ std::lock_guard guard{state_mutex_}; connected_ = false; } BOOST_LOG_SEV(lg(), warn) << "Connection lost - server may have closed the connection";To ensure the client is in a clean state after a read failure, it's good practice to explicitly close the connection socket. A read failure, especially one that results from the server closing the connection, means the connection is no longer viable. This change makes the error handling here consistent with the catch (
const boost::system::system_error& e) block.{ std::lock_guard guard{state_mutex_}; connected_ = false; } if (conn_) { conn_->close(); } BOOST_LOG_SEV(lg(), warn) << "Connection lost - server may have closed the connection"; - COMPLETED projects/ores.comms/tests/net_client_tests.cpp
- priority: medium
- status: FIXED
- Comment on lines +251 to +254
// We can verify by attempting to send a dummy request - it should fail // Note: This test primarily verifies that server->stop() successfully // cancelled all sessions without hanging, which is the main goal of // the session cancellation feature.
The comment here correctly identifies a way to strengthen this test. By adding an explicit check to ensure clients can no longer communicate after the server has been stopped, the test will more comprehensively verify the end-to-end behavior of the session cancellation, including the effect on the client side.
// We can verify by attempting to send a dummy request - it should fail. BOOST_LOG_SEV(lg, info) << "Verifying clients can no longer communicate"; for (auto& client : clients) { ores::comms::protocol::frame dummy_request{ ores::comms::protocol::message_type::handshake_request, 0, {}}; auto result = co_await client->send_request(dummy_request); CHECK(!result.has_value()); CHECK(!client->is_connected()); }
- COMPLETED projects/ores.service/src/main.cpp
- Old implementation plan from Qwen
Here's a **step-by-step implementation plan** to **cancel all active sessions when the server stops**—building cleanly on your current Asio coroutine-based server design. --- ### 🎯 Goal When `server::stop()` is called (e.g., via SIGINT or programmatic shutdown): 1. **Immediately cancel** all ongoing session I/O operations (reads/writes, etc.). 2. **Gracefully terminate** all sessions without waiting indefinitely. 3. Preserve your existing cancellation architecture (based on `cancellation_signal`). --- ### ✅ High-Level Strategy Use a **hierarchical cancellation model**: - The **server owns a root cancellation signal**. - Each **session inherits a slot** from this root signal. - When the server stops, it **emits cancellation**, which **propagates to all sessions**. This avoids manual session tracking and leverages Asio's built-in cancellation propagation. --- ## 📝 Implementation Plan ### Step 1: Add a Root Cancellation Signal for Sessions In `server.hpp`, add: ```cpp private: boost::asio::cancellation_signal session_stop_signal_; ``` > This signal will be used to cancel **all sessions** simultaneously. --- ### Step 2: Expose a Cancellation Slot to Sessions Add a **public (or private, if sessions are friends)** method: ```cpp public: boost::asio::cancellation_slot get_session_cancellation_slot() const noexcept { return session_stop_signal_.slot(); } ``` > Alternatively, make `session` a `friend class` and pass the slot directly in `accept_loop`. --- ### Step 3: Pass the Slot to Each Session In `accept_loop`, when creating a session: ```cpp auto sess = std::make_shared<session>( std::move(conn), options_.server_identifier, dispatcher_, get_session_cancellation_slot() // ← pass the slot ); ``` > You'll need to **update `session`'s constructor** to accept and store this slot (or a copy of it). --- ### Step 4: Update `session` to Use the Cancellation Slot In `session.hpp`: ```cpp class session { public: session( std::unique_ptr<connection> conn, std::string server_id, std::shared_ptr<protocol::message_dispatcher> dispatcher, boost::asio::cancellation_slot stop_slot); // ← new parameter private: boost::asio::cancellation_slot stop_slot_; // ... other members }; ``` In `session.cpp` constructor: ```cpp session::session( std::unique_ptr<connection> conn, std::string server_id, std::shared_ptr<protocol::message_dispatcher> dispatcher, boost::asio::cancellation_slot stop_slot) : conn_(std::move(conn)) , server_id_(std::move(server_id)) , dispatcher_(std::move(dispatchor)) , stop_slot_(std::move(stop_slot)) { } ``` --- ### Step 5: Bind Session I/O to the Cancellation Slot In `session::run()` or wherever you do `co_await socket.async_read(...)`: ```cpp // Example: reading from SSL stream co_await conn_->ssl_stream().async_read_some( buffer, boost::asio::bind_cancellation_slot( stop_slot_, boost::asio::use_awaitable)); ``` > Repeat for **all** async operations in the session (read, write, timers, etc.). This ensures that when the server emits cancellation, **all session I/O is aborted**. > 💡 Tip: If you have a session-level read/write loop, you only need to bind the **outermost** `co_await`—cancellation will propagate. --- ### Step 6: Update `server::stop()` to Cancel Sessions Too Modify your existing `stop()`: ```cpp void server::stop() { BOOST_LOG_SEV(lg(), info) << "Stopping server..."; // Cancel all sessions first session_stop_signal_.emit(boost::asio::cancellation_type::all); // Then cancel accept loop stop_signal_.emit(boost::asio::cancellation_type::all); } ``` > Order doesn't strictly matter, but canceling sessions first is more logical. --- ### Step 7: (Optional) Add Session Cleanup Logging In `session::run()`, catch `operation_aborted` to log graceful exit: ```cpp try { co_await /* session work */; } catch (const boost::system::system_error& e) { if (e.code() == boost::asio::error::operation_aborted) { BOOST_LOG_SEV(lg(), debug) << "Session cancelled."; } else { BOOST_LOG_SEV(lg(), error) << "Session error: " << e.what(); } } ``` --- ### ✅ Benefits of This Approach - **No manual session tracking** (no list of sessions to iterate over). - **Automatic propagation**: One `emit()` cancels everything. - **Safe and efficient**: Uses Asio's native cancellation. - **Composable**: Works with nested coroutines inside sessions. --- ### 🔒 Safety Notes - The `session_stop_signal_` lives as long as the `server` object. - Sessions capture a **slot**, not a reference to the signal—safe even if the session outlives the emit (which it won't, because you're stopping the server). - No race conditions: `emit()` is thread-safe and idempotent. --- ### 🧪 Testing 1. Start server, open 2 connections. 2. Send Ctrl+C. 3. Verify: - "Stopping server..." log appears. - Both sessions log "Session cancelled." (or similar). - Program exits promptly. --- Let me know if you'd like the full code patches for `session.hpp/cpp` or help adapting this to your exact `connection`/`ssl_stream` usage!
COMPLETED Create a variability service for feature flags code
We should not expose the repository directly. Instead, consumers should use a service interface which queries the repository as needed.
COMPLETED Add tests to diagrams code
It is helpful so we can see how we are doing in terms of coverage visually.
COMPLETED Add messaging to variability code
At present we left the messaging code related to feature flags in accounts.
- Implementation Plan
Move feature flags messaging infrastructure from ores.accounts to ores.variability to align with domain ownership.
Strategy:
- Bump protocol from v5 to v6 (breaking change)
- Assign variability subsystem range 0x3000-0x3FFF
- Move feature flags messages from 0x200D/0x200E to 0x3000/0x3001
- Use linux-gcc-debug preset (clang has issues)
Tasks:
[ ]Update protocol version and message types- Change protocol_version_major from 5 to 6
- Add variability range 0x3000-0x3FFF
- Move feature flags to 0x3000/0x3001
- Remove old 0x200D/0x200E codes
- File:
projects/ores.comms/include/ores.comms/protocol/message_types.hpp
[ ]Create variability messaging infrastructure- Create
projects/ores.variability/include/ores.variability/messaging/directory - Create
projects/ores.variability/src/messaging/directory - Move feature_flags_protocol.hpp/cpp from accounts to variability
- Update namespace to
ores::variability::messaging - Update message type references to 0x3000/0x3001
- Create variability_message_handler.hpp/cpp
- Create registrar.hpp/cpp with range {0x3000, 0x3FFF}
- Create protocol.hpp aggregation header
- Create
[ ]Update CMake dependencies- Add
ores.comms.libto variability dependencies (PUBLIC) - Add
ores.variability.libto service dependencies - Files:
projects/ores.variability/src/CMakeLists.txt,projects/ores.service/src/CMakeLists.txt
- Add
[ ]Remove feature flags from accounts- Remove feature flags includes from account_service.hpp
- Remove list_feature_flags() method from account_service
- Remove feature_flags_repo_ member from account_service
- Remove feature flags handler from accounts_message_handler
- Remove feature_flags_protocol.hpp include from protocol.hpp
- Delete moved protocol files from accounts
- Files: account_service.hpp/cpp, accounts_message_handler.hpp/cpp, protocol.hpp
[ ]Register variability handlers- Add variability registrar include to application.cpp
- Call
ores::variability::messaging::registrar::register_handlers() - File:
projects/ores.service/src/app/application.cpp
[ ]Update documentation- Add variability to CLAUDE.md architecture section
[ ]Build and verify- Build:
cmake --build --preset linux-gcc-debug - Test runtime with message type 0x3000
- Run all tests
- Update any tests referencing old protocol version
- Build:
Breaking Changes:
- Protocol v5 → v6
- Message types changed (0x200D/0x200E → 0x3000/0x3001)
- All clients must update
COMPLETED Split protocol.hpp into components code
Rationale: implemented.
These files will become too large as we add more entities.
COMPLETED Create handshake service in comms code
We seem to have relationships all over the place to handle handshaking. It makes more sense to encapsulate all of that in a handshake service that serves both ends of the handshake.
Notes:
- make the uses of "messaging" and "protocol" consistent across projects.
- document namespaces in comms.
COMPLETED Add support for feature flags code
Rationale: implemented.
We need a way to know if we are in bootstrap mode or not. Implement a generic mechanism for feature flags.
Example chrome flag:
Temporarily unexpire M139 flags.
Temporarily unexpire flags that expired as of M139. These flags will be removed soon. – Mac, Windows, Linux, ChromeOS, Android
#temporary-unexpire-flags-m139
Components:
- name: human readable
- description
- id
COMPLETED Implement client heartbeat for disconnect detection code
Currently, the client only detects server disconnections when attempting to send a request. If the server closes the connection (e.g., via graceful shutdown), the client remains unaware until the next user action. This creates poor user experience where the Qt application appears connected but operations fail unexpectedly.
A heartbeat mechanism would allow the client to proactively detect when the server closes the connection and notify the application immediately, enabling proper UI updates (connection status indicators, reconnection prompts, etc.).
Business value:
- Improved user experience with immediate disconnect notifications
- Better error handling and recovery workflows
- Reduced user frustration from "silent" disconnections
- Foundation for future features (connection quality monitoring, auto-reconnect)
Acceptance criteria:
- Client sends periodic heartbeat/ping messages to server (configurable interval)
- Server responds to heartbeat messages with minimal overhead
- Client detects failed heartbeats and marks connection as disconnected
- Client provides callback/signal mechanism for disconnect notification
- Qt application can register disconnect callback to update UI
- Heartbeat can be enabled/disabled via client configuration
- Heartbeat does not interfere with normal request/response operations
- Logging clearly indicates heartbeat activity and failures
Implementation considerations:
- Add ping/pong message types to protocol (lightweight, no payload)
- Use async timer in client to trigger periodic heartbeats
- Heartbeat interval should be configurable (default: 30 seconds)
- Server should handle ping messages in message dispatcher
- Client should expose disconnect_callback_t for applications to register
- Ensure thread-safe callback invocation for Qt integration
- Consider making heartbeat optional to support existing clients
Files likely affected:
projects/ores.comms/include/ores.comms/protocol/messages.hpp- Add ping/pong typesprojects/ores.comms/include/ores.comms/net/client.hpp- Add callback mechanismprojects/ores.comms/src/net/client.cpp- Implement heartbeat coroutineprojects/ores.comms/src/protocol/message_dispatcher.cpp- Handle ping messagesprojects/ores.qt/...- Register disconnect callback, update UI
Related to completed story: "Implement session cancellation" - that story addresses graceful server shutdown, this story addresses client-side detection of that shutdown.
Notes:
- move service to service directory.
- follow the "_protocol" convention for file names with messages.
- Session: Address race condition review comment
The heartbeat implementation has a critical race condition where
run_heartbeat()andsend_request()can execute concurrently, leading to two independent request-response cycles on the same socket without synchronization. This also needs to support future "listen/notify" server-push notifications.Solution: Unified message loop with correlation IDs - single reader dispatches incoming frames by type, writes serialized via strand, correlation IDs match responses to requests.
- Tasks
[X]1.1 Addcorrelation_idto frame structure- Add
uint32_t correlation_idfield toprotocol::frame - Update frame serialization/deserialization
- Bump protocol version if needed
- Add
[X]1.2 Update message types if needed- Verify ping/pong exist (confirmed: 0x0005, 0x0006)
- Notification types deferred until listen/notify implementation
[X]2.1 Createresponse_channelclass- Single-value async channel using Boost.Asio primitives
set_value(frame)- producer side (reader loop)set_error(error_code)- for timeouts/disconnectsawaitable<expected<frame, error_code>> get()- consumer side- Use
asio::steady_timeras signaling primitive
[X]2.2 Createpending_request_mapclass- Thread-safe map:
correlation_id→response_channel register(correlation_id)→response_channel&complete(correlation_id, frame)- called by readerfail(correlation_id, error)- called on timeout/disconnectfail_all(error)- called on connection loss
- Thread-safe map:
[X]3.1 Add write strand to clientasio::strand<asio::any_io_executor> write_strand_- Initialize in constructor
[X]3.2 Createwrite_frame()helper- Posts write operation to strand
- Returns awaitable that completes when write is done
[X]4.1 Implementrun_message_loop()coroutine- Single reader, loops reading frames
- Dispatches by message type (response/pong → complete, notification → callback)
- On read error →
fail_all(), set disconnected
[ ]4.2 Add centralized timeout handling (deferred)- Read with timeout using
asio::steady_timer - Configurable default timeout
- Can be added later when needed
- Read with timeout using
[X]5. Refactorsend_request()- Generate correlation_id
- Register in pending_requests_map
- Post write to strand
co_awaitresponse channel
[X]6. Refactorrun_heartbeat()to use new infrastructure- Use unified request/response pattern
- Timeout triggers disconnect callback
[X]7. Update server to echocorrelation_id- Ping handler returns pong with same correlation_id
- All response handlers copy correlation_id from request
[-]8.1 Add notification callback type (deferred to listen/notify story)notification_callback_tfor server-push messages
[X]8.2 Keepdisconnect_callback_t- Invoked by message loop on connection loss
[X]9.1 Start message loop on connectco_spawntherun_message_loop()after connection
[X]9.2 Clean shutdown- Cancel message loop, fail pending requests, invoke disconnect callback
[ ]10.1 Update existing tests[ ]10.2 Add new tests- Concurrent requests with correlation IDs
- Request timeout handling
- Heartbeat timeout triggers disconnect
- Write serialization
- Tasks
COMPLETED Multi-threaded scenarios with comms code
Rationale: Handled as part of heartbeat work.
At present we are relying on request-response patterns: the client sends a request and the next frame coming in is the response. However, in the future we will have many dialog windows open, each of them sending requests and awaiting responses. It will not be possible to know what response is coming back from what window. We need to take this into account.
COMPLETED Disconnect closes currencies window code
Rationale: Handled as part of heartbeat work.
It should just disable the icons, etc.
COMPLETED Pressing disconnect crashes client code
We need to debug this crash. Disconnect works as a disconnect from the server side. Works sometimes, sometimes crashes.
COMPLETED Create a component creator skill infra
It is painful to have to create components. Teach LLMs how to do it.
CANCELLED Add listen/notify support code
Rationale: this story is a very large epic and we need to break it down into more manageable pieces.
When data changes for a given entity in the database and we have the dialog of that entity open, we need to make the reload button a different colour (suggest a colour). For this we need to listen/notify in the database and then send a message to the client. This requires a change at the protocol because at present we send a request from the client first and then expect a response. This is more like a callback where the handler will call a callback when a certain message is received.
Notes:
- need to save the session on the listener to acquire the connection. Actually this is not a very good idea as we are not really using the pool properly.
- we need reconnect logic to handle cases where the DB connection is down. In addition, we need to be able to notify the client. There needs to be an event type for this. The listener needs to periodically check the status of the DB connection; if down, attempt to reconnect, notify listeners that connection is down. On reconnect we need to re-listen to all the topics we had subscribed before.
- when we map application level events to low-level DB events, these should really live inside each component. These follow the same pattern as "messaging" in namespace "eventing".
- we need a bridge which listens to socket events and connects to remote event bus to distribute the events.
- Postgres listener is an adaptor of some kind.
- Rename service to
ores.eventsor maybeores.eventing. Qwen:
The term "notification" strongly implies passive alerts (e.g., “you have a message”) or user-facing banners/toasts. But your subsystem is far more powerful:
- It’s a real-time event distribution fabric,
- It carries domain events (TradeUpdated, FxPriceChanged, ReportCompleted),
- It enables reactive data synchronization, UI auto-refresh, and system-to-system coordination.
Calling it notification may:
- Understate its role,
- Confuse it with UI toast systems or email-style alerts,
- Imply it’s only for “out-of-band” signals, not core data flow.
Links:
- Revised requirements from Qwen
### 📜 **End-to-End Application Message Bus Specification** #### **1. Overall Principle** The system shall support **real-time, typed, parameterized event delivery** from a **central C++23 service** to **multiple client applications** (Qt UI, REPL, Wt, grid engines, etc.) over an existing **bespoke binary protocol**. The design must be **agnostic to event origins** and **decoupled from any specific UI framework**. --- #### **2. Service Responsibilities** The **central service** shall act as a **unified event broker** and must: - **Aggregate events from arbitrary internal sources**, including but not limited to: - PostgreSQL `LISTEN/NOTIFY`, - Background job completion (e.g., report generation), - Grid computation results, - Scheduled tasks, - User or system-triggered domain events. - **Normalize all events** into a uniform model consisting of: - `event_type`: a stable, logical string identifier (e.g., `"fx_price"`, `"report_completed"`), - `key`: a string that identifies the specific entity or scope (e.g., `"EUR/USD"`, `"report_123"`), - `payload`: a binary-serialized representation of the event data (using the existing serialization format). - **Maintain per-client subscription state**, tracking which clients are subscribed to which `(event_type, key)` pairs. - **Filter and deliver** only relevant events to each connected client: - When a new event is produced internally, the service forwards it **only** to clients subscribed to its `(event_type, key)`. - Delivery uses the existing **binary protocol frame format**. - **Support dynamic subscription management** via two new protocol message types: - `SUBSCRIBE(event_type: string, key: string)` → register interest, - `UNSUBSCRIBE(event_type: string, key: string)` → remove interest. - **Ensure no event source leaks into the protocol**—PostgreSQL is an implementation detail invisible to clients. --- #### **3. Client Responsibilities** Each **client process** shall provide a **local, in-process message bus** that: - Offers a **type-safe C++23 API** for subscribing to domain events: ```cpp auto sub = bus.subscribe<ReportCompleted>("report_123", handler); ``` - **Maps event types to logical names** via a user-defined trait: ```cpp template<> struct event_traits<ReportCompleted> { static constexpr const char* name = "report_completed"; }; ``` - **Automatically manages remote subscriptions**: - On the **first subscription** to a given `(event_type, key)`, sends a `SUBSCRIBE` message to the service. - On the **last unsubscription**, sends an `UNSUBSCRIBE`. - **Delivers incoming notifications** to all matching local subscribers: - Uses **Boost.Asio executors** to support asynchronous, thread-safe delivery. - Does **not assume a UI framework**—subscribers provide their own execution context. - Supports **RAII-based lifetime management**: subscriptions are automatically cleaned up when the subscription object is destroyed. - Is **header-only, portable, and coroutine-friendly**, using only C++23 and Boost.Asio. --- #### **4. Protocol Contract (Client ↔ Service)** The **only coupling** between client and service is the **binary protocol**, which must support: | Message Type | Direction | Purpose | |----------------|------------------|---------------------------------------------| | `SUBSCRIBE` | Client → Service | Express interest in `(event_type, key)` | | `UNSUBSCRIBE` | Client → Service | Revoke interest | | `NOTIFICATION` | Service → Client | Deliver event: `(event_type, key, payload)` | - The `payload` must be deserializable into the corresponding C++ event struct using the **existing serialization mechanism** (e.g., your `serialize()`/`deserialize()` functions). - The protocol must be **backward-compatible** and **extensible** to new event types without breaking existing clients. --- #### **5. Non-Functional Requirements** - **Decoupling**: No UI framework (Qt, Wt, etc.) or event source (PostgreSQL, etc.) dependencies in the core bus logic. - **Efficiency**: Support hundreds of concurrent subscriptions with low overhead; high-frequency events (e.g., FX) must not overwhelm the system. - **Safety**: Thread-safe publishing and subscription; no dangling callbacks. - **Evolvability**: Adding a new event type requires only: - A C++ struct, - A `event_traits` specialization, - Service-side event production (no bus or protocol changes). --- This specification ensures you can **start with PostgreSQL-driven notifications** today and **seamlessly integrate grid engines, async reports, or external event streams** tomorrow—without redesigning your messaging layer. Let me know if this captures your intent, and we can proceed to **design the interfaces** or **draft the protocol extensions**. - Architectural Plan
We will implement a subscription-based model to support scaling to many entities. A dedicated component
ores.notificationwill handle the database listening and subscription management, keepingores.commsfocused on transport.- 1. Database Layer (Postgres)
- Mechanism: Postgres
TRIGGERonINSERT/UPDATE/DELETEfor entities. - Channels: One channel per entity (e.g.,
ores_currencies). - Payload: A simple JSON indicating the entity name and the timestamp of the change (roughly corresponding to `valid_to` of the affected row). No IDs are needed in the notification payload. Clients will be responsible for refreshing the data, potentially querying the database for changes since the provided timestamp.
- Mechanism: Postgres
- 2. ores.notification (New Component)
- Responsibility: Manages database listeners and client subscriptions.
- PostgresListener:
- Manages a dedicated connection for
LISTEN/NOTIFY. - Runs a dedicated thread to poll/wait for notifications.
- Parses incoming JSON payloads.
- Manages a dedicated connection for
- SubscriptionManager:
- Maintains a map of
Topic -> List<SessionID>. - Handles
subscribeandunsubscribelogic. - Dispatches notifications to relevant sessions via the Server.
- Maintains a map of
- 3. ores.comms (Protocol)
- New Message Types:
SubscribeRequest { topic: string }SubscribeResponse { success: bool }NotificationMessage { topic: string, payload: string (JSON) }
- Protocol Version: Bump to v7 (breaking change).
- New Message Types:
- 4. ores.service (Wiring)
- Initializes
NotificationService(fromores.notification). - Connects the service to the
Serverinstance. - Registers the
SubscribeRequesthandler.
- Initializes
- 5. Client Layer (Qt)
- ClientManager: Exposes
subscribe(topic)andnotificationReceivedsignal. - UI Controllers:
- Subscribe to relevant topics when windows are opened.
- Unsubscribe (optionally) when closed.
- UX: Visual indicator (e.g., "Reload" button turns orange/red or a badge appears) rather than auto-reloading, avoiding jarring UI shifts.
- ClientManager: Exposes
- 1. Database Layer (Postgres)
- Tasks
- COMPLETED Database Infrastructure
- Create a migration to add
LISTEN/NOTIFYtriggers forcurrencies. - Define the simplified JSON payload structure for triggers (entity name, timestamp).
- Create a migration to add
- STARTED Component Creation: ores.notification
- Create scaffolding for
ores.notification(CMakeLists, file structure). - Implement
PostgresListenerusingsqlgen(or extendsqlgenif needed for raw handle). - Implement
SubscriptionManager.
- Create scaffolding for
- Protocol Updates (ores.comms)
- Bump protocol version to v7.
- Add
SubscribeRequest,SubscribeResponse,NotificationMessagetypes. - Update serialization logic for the simplified
NotificationMessagepayload.
- Server Integration (ores.service)
- Wire
ores.notificationinto the application startup. - Implement the handler for
SubscribeRequest. - enable broadcasting from
SubscriptionManagertoServersessions.
- Wire
- Client Implementation (ores.qt)
- Update
ClientManagerto handle unsolicitedNotificationMessageframes. - Add
subscribe()method toClientManager. - Update
CurrencyControllerto subscribe to "currencies". - Update
CurrencyMdiWindowto show "Stale Data" indicator on notification.
- Update
- COMPLETED Database Infrastructure
- Listen code from deepseek
#include <sqlgen.h> #include <libpq-fe.h> #include <thread> #include <iostream> class TableWatcher { private: sqlgen::Connection conn_; std::thread listener_thread_; bool running_; public: TableWatcher(const std::string& connection_string) : conn_(connection_string), running_(false) {} void start() { running_ = true; listener_thread_ = std::thread(&TableWatcher::listen_loop, this); } void stop() { running_ = false; if (listener_thread_.joinable()) { listener_thread_.join(); } } private: void listen_loop() { // Get raw PGconn for async operations auto raw_conn = conn_.native_handle(); // You might need to expose this // Listen to channel PGresult* res = PQexec(raw_conn, "LISTEN table_updates;"); if (PQresultStatus(res) != PGRES_COMMAND_OK) { PQclear(res); return; } PQclear(res); while (running_) { // Non-blocking check for notifications PQconsumeInput(raw_conn); PGnotify* notify; while ((notify = PQnotifies(raw_conn)) != nullptr) { handle_notification(notify); PQfreemem(notify); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } void handle_notification(PGnotify* notify) { std::cout << "Received notification: " << notify->relname << " - " << notify->extra << std::endl; // Parse the extra data (usually JSON) and handle accordingly // {"table": "users", "operation": "INSERT", "id": 123} } };
COMPLETED Move context to database code
We have a number of classes in repository when they are general database entities: context, context factory etc. We should just leave the repository specific code in that namespace.
COMPLETED Use std::string_view for loggers code
We are using std::string for no good reason other than historical precedent.
Before we create too much code relying on this pattern, update the code to use
string views.
Gemini description:
This pull request introduces a significant refactoring of the logging infrastructure across the entire project. The primary goal is to enhance performance by transitioning from
std::stringtostd::string_viewfor logger names, thereby minimizing string copies. This change is systematically applied to logger declarations and usage in a wide array of components. Additionally, the PR incorporates several modern C++ idioms, refines documentation, and cleans up redundant header inclusions, contributing to a more efficient and maintainable codebase.Highlights
- Logging Optimization: Replaced
std::stringwithstd::string_viewfor logger names across the codebase to improve performance by avoiding unnecessary string allocations and copies. This involved updating themake_loggerfunction signature and modifying numerous class-level logger instantiations.- Modern C++ Refactoring: Updated repository helper functions (
execute_read_query,execute_write_query,ensure_success,make_timestamp,generate_create_table_sql) to accept a logger instance directly (utility::log::logger_t&) instead of a string literal for the logger name, streamlining logging calls. Also, adoptedstd::ranges::any_ofin some areas for more modern C++ range-based algorithms.- Codebase Cleanup: Removed various unused header includes across multiple projects (
ores.cli,ores.comms,ores.qt,ores.shell,ores.variability) to reduce compilation times and improve code clarity.- Documentation and Sprint Updates: Updated product and sprint backlog documentation (
doc/agile/product_backlog.org,doc/agile/v0/sprint_backlog_06.org) to reflect new logging features and progress on development tasks.- Conceptual Renaming: Renamed
ores.eventing/modeling/ores.notification.pumltoores.eventing/modeling/ores.eventing.pumland updated its content to align with 'eventing' terminology.
Notes:
- rename protocol tests.
COMPLETED Fix issues with logging after string_view change code
It seems the component is now blank. The test logging is also not very sensible, using the tags as the component name.
COMPLETED Remove bootstrap mode from context code
We still have:
/** * @brief Set the bootstrap mode flag. * * Should be called during application initialization after checking bootstrap state. */ void set_bootstrap_mode(bool mode) { bootstrap_mode_ = mode; } /** * @brief Check if the system is in bootstrap mode. * * @return true if system is in bootstrap mode (no admin accounts exist), false otherwise */ bool is_in_bootstrap_mode() const { return bootstrap_mode_; }
This was temporary until variability was introduced. Check if is still in use and if not remove it.
Notes:
- the problem is that we created the variability feature flags service, but it is not at the right level of abstraction for determining if a feature is enabled or not. As we did for the messaging, we need a list of features hard-coded into a class whenever a feature cuts across components.
- Qwen terminology analysis
Based on the content of the page [MDE and Variability Modeling](https://masd-project.github.io/progen/docs/mde_and_variability_modeling.html), which discusses feature-oriented modeling, variability, and software product lines, a class that centrally holds all feature flags in your system should reflect its role as the source of variability decisions or the feature configuration for a given product or runtime instance.
Given your context—C++23, event-driven architecture, and a preference for clear, structured, domain-aligned naming—here are a few well-motivated options:
### Recommended Names
- `FeatureConfiguration`
– Clear, concise, and emphasizes that this class captures the current selection of features (i.e., a configuration derived from the variability model).
- `FeatureSet`
– Suggests a collection of enabled/disabled features; simple and idiomatic in C++.
- `VariabilityContext`
– Aligns closely with MDE terminology from the page; useful if you treat feature activation as part of a broader runtime or build-time context.
- `ProductFeatures`
– Emphasizes the product-line aspect ("product" as in software product line), which matches your use of progen for product generation.
- `FeatureProfile`
– Implies a named or predefined combination of features (e.g., “debug profile”, “lite profile”), supporting gradual rollouts or user segments.
### Best Fit for Your System
Given your emphasis on product generation, modularity, and runtime feature control (e.g., testing with 10% of users), `FeatureConfiguration` is likely the strongest choice:
- It avoids overloading “profile” (which may imply performance).
- It’s more precise than “set” (which is purely structural).
- It aligns with variability modeling concepts while remaining intuitive to C++ developers.
You might place it in a dedicated namespace like `system::features` or `app::config`.
COMPLETED Add tests for messaging handler code
We need tests in comms which create a test request and response and a test handler.
COMPLETED Add retry algorithm to client code
It would be nice to reconnect automatically.
Notes:
- we need some kind of feedback to UI stating we are retrying.
- reconnects successfully but UI feedback is not quite right: we do not show we are disconnected, etc.
COMPLETED Fix valgrind leaks after string view changes to logging code
<b>MPK</b> ==50939== 24 bytes in 1 blocks are still reachable in loss record 4 of 17
==50939== at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==50939== by 0x581927B: boost::detail::shared_count::shared_count<boost::detail::thread_data_base>(boost::detail::thread_data_base*) (shared_count.hpp:146)
==50939== by 0x5817F4B: void boost::detail::sp_pointer_construct<boost::detail::thread_data_base, boost::detail::thread_data_base>(boost::shared_ptr<boost::detail::thread_data_base>*, boost::detail::thread_data_base*, boost::detail::shared_count&) (shared_ptr.hpp:205)
==50939== by 0x5816F76: boost::shared_ptr<boost::detail::thread_data_base>::shared_ptr<boost::detail::thread_data_base>(boost::detail::thread_data_base*) (shared_ptr.hpp:283)
==50939== by 0x5815723: void boost::shared_ptr<boost::detail::thread_data_base>::reset<boost::detail::thread_data_base>(boost::detail::thread_data_base*) (shared_ptr.hpp:519)
==50939== by 0x580FF74: boost::detail::make_external_thread_data() (thread.cpp:236)
==50939== by 0x580FFA7: boost::detail::get_or_make_current_thread_data() (thread.cpp:247)
==50939== by 0x5811498: boost::detail::add_thread_exit_function(boost::detail::thread_exit_function_base*) (thread.cpp:710)
==50939== by 0x562D68B: void boost::this_thread::at_thread_exit<boost::log::v2s_mt_posix::sources::aux::get_severity_level()::{lambda()#1}>(boost::log::v2s_mt_posix::sources::aux::get_severity_level()::{lambda()#1}) (thread.hpp:856)
==50939== by 0x562D601: boost::log::v2s_mt_posix::sources::aux::get_severity_level() (severity_level.cpp:67)
==50939== by 0x4A05A9: boost::log::v2s_mt_posix::sources::aux::severity_level<ores::utility::log::severity_level>::set_value(ores::utility::log::severity_level) (severity_feature.hpp:136)
==50939== by 0x49E8EE: boost::log::v2s_mt_posix::record boost::log::v2s_mt_posix::sources::basic_severity_logger<boost::log::v2s_mt_posix::sources::basic_channel_logger<boost::log::v2s_mt_posix::sources::basic_logger<char, boost::log::v2s_mt_posix::sources::severity_channel_logger_mt<ores::utility::log::severity_level, std::basic_string_view<char, std::char_traits<char> > >, boost::log::v2s_mt_posix::sources::multi_thread_model<boost::log::v2s_mt_posix::aux::light_rw_mutex> >, std::basic_string_view<char, std::char_traits<char> > >, ores::utility::log::severity_level>::open_record_unlocked<boost::parameter::aux::tagged_argument_list_of_1<boost::parameter::aux::tagged_argument<boost::log::v2s_mt_posix::keywords::tag::severity, ores::utility::log::severity_level const> > >(boost::parameter::aux::tagged_argument_list_of_1<boost::parameter::aux::tagged_argument<boost::log::v2s_mt_posix::keywords::tag::severity, ores::utility::log::severity_level const> > const&) (severity_feature.hpp:255)
==50939== by 0x49CED4: boost::log::v2s_mt_posix::record boost::log::v2s_mt_posix::sources::basic_composite_logger<char, boost::log::v2s_mt_posix::sources::severity_channel_logger_mt<ores::utility::log::severity_level, std::basic_string_view<char, std::char_traits<char> > >, boost::log::v2s_mt_posix::sources::multi_thread_model<boost::log::v2s_mt_posix::aux::light_rw_mutex>, boost::log::v2s_mt_posix::sources::features<boost::log::v2s_mt_posix::sources::severity<ores::utility::log::severity_level>, boost::log::v2s_mt_posix::sources::channel<std::basic_string_view<char, std::char_traits<char> > > > >::open_record<boost::parameter::aux::tagged_argument_list_of_1<boost::parameter::aux::tagged_argument<boost::log::v2s_mt_posix::keywords::tag::severity, ores::utility::log::severity_level const> > >(boost::parameter::aux::tagged_argument_list_of_1<boost::parameter::aux::tagged_argument<boost::log::v2s_mt_posix::keywords::tag::severity, ores::utility::log::severity_level const> > const&) (basic_logger.hpp:463)
==50939== by 0x48CB6B9: ores::testing::database_lifecycle_listener::testRunStarting(Catch::TestRunInfo const&) (database_lifecycle_listener.cpp:33)
==50939== by 0x5E0AC0: Catch::MultiReporter::testRunStarting(Catch::TestRunInfo const&) (catch_reporter_multi.cpp:89)
==50939== by 0x59D153: Catch::RunContext::RunContext(Catch::IConfig const*, Catch::Detail::unique_ptr<Catch::IEventListener>&&) (catch_run_context.cpp:216)
==50939== by 0x547BAE: Catch::(anonymous namespace)::TestGroup::TestGroup(Catch::Detail::unique_ptr<Catch::IEventListener>&&, Catch::Config const*) (catch_session.cpp:81)
==50939== by 0x549921: Catch::Session::runInternal() (catch_session.cpp:378)
==50939== by 0x54941F: Catch::Session::run() (catch_session.cpp:306)
==50939== by 0x5268E4: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==50939== by 0x52619E: main (main.cpp:36)
==50939==
{
<insert_a_suppression_name_here>
Memcheck:Leak
match-leak-kinds: reachable
fun:_Znwm
fun:_ZN5boost6detail12shared_countC1INS0_16thread_data_baseEEEPT_
fun:_ZN5boost6detail20sp_pointer_constructINS0_16thread_data_baseES2_EEvPNS_10shared_ptrIT_EEPT0_RNS0_12shared_countE
fun:_ZN5boost10shared_ptrINS_6detail16thread_data_baseEEC1IS2_EEPT_
fun:_ZN5boost10shared_ptrINS_6detail16thread_data_baseEE5resetIS2_EEvPT_
fun:_ZN5boost6detail25make_external_thread_dataEv
fun:_ZN5boost6detail31get_or_make_current_thread_dataEv
fun:_ZN5boost6detail24add_thread_exit_functionEPNS0_25thread_exit_function_baseE
fun:_ZN5boost11this_thread14at_thread_exitIZNS_3log12v2s_mt_posix7sources3aux18get_severity_levelEvEUlvE_EEvT_
fun:_ZN5boost3log12v2s_mt_posix7sources3aux18get_severity_levelEv
fun:_ZN5boost3log12v2s_mt_posix7sources3aux14severity_levelIN4ores7utility3log14severity_levelEE9set_valueES8_
fun:_ZN5boost3log12v2s_mt_posix7sources21basic_severity_loggerINS2_20basic_channel_loggerINS2_12basic_loggerIcNS2_26severity_channel_logger_mtIN4ores7utility3log14severity_levelESt17basic_string_viewIcSt11char_traitsIcEEEENS2_18multi_thread_modelINS1_3aux14light_rw_mutexEEEEESE_EESA_E20open_record_unlockedINS_9parameter3aux25tagged_argument_list_of_1INSP_15tagged_argumentINS1_8keywords3tag8severityEKSA_EEEEEENS1_6recordERKT_
fun:_ZN5boost3log12v2s_mt_posix7sources22basic_composite_loggerIcNS2_26severity_channel_logger_mtIN4ores7utility3log14severity_levelESt17basic_string_viewIcSt11char_traitsIcEEEENS2_18multi_thread_modelINS1_3aux14light_rw_mutexEEENS2_8featuresIJNS2_8severityIS8_EENS2_7channelISC_EEEEEE11open_recordINS_9parameter3aux25tagged_argument_list_of_1INSR_15tagged_argumentINS1_8keywords3tag8severityEKS8_EEEEEENS1_6recordERKT_
fun:_ZN4ores7testing27database_lifecycle_listener15testRunStartingERKN5Catch11TestRunInfoE
fun:_ZN5Catch13MultiReporter15testRunStartingERKNS_11TestRunInfoE
fun:_ZN5Catch10RunContextC1EPKNS_7IConfigEONS_6Detail10unique_ptrINS_14IEventListenerEEE
fun:_ZN5Catch12_GLOBAL__N_19TestGroupC1EONS_6Detail10unique_ptrINS_14IEventListenerEEEPKNS_6ConfigE
fun:_ZN5Catch7Session11runInternalEv
fun:_ZN5Catch7Session3runEv
fun:_ZN5Catch7Session3runIcEEiiPKPKT_
fun:main
}
COMPLETED Remove enum exception code
We should use logic error instead:
throw std::logic_error("Definition for system_flag not found.");
Or perhaps out_of_range.
Links:
- std::logic_error: "Defines a type of object to be thrown as exception. It
reports errors that are a consequence of faulty logic within the program such
as violating logical preconditions or class invariants and may be preventable.
No standard library components throw this exception directly, but the
exception types
std::invalid_argument,std::domain_error,std::length_error,std::out_of_range,std::future_error, andstd::experimental::bad_optional_accessare derived fromstd::logic_error." - std::out_of_range: "Defines a type of object to be thrown as exception. It reports errors that are consequence of attempt to access elements out of defined range."
COMPLETED Entity syntax refactor snags code
We refactored ores.cli to be "entity oriented", so that commands such as export, import etc belonged directly to an entity. In this case there should be no need to supply the entity in the command line since it is part of the command. However, we still see the entity enum in the code.
Notes:
- this is not quite done yet. We need to split
add_options.
CANCELLED Experiment with simple code generation code
Rationale: Claude code is now good enough.
It should be easy enough to add a simple code generator that creates the basic infrastructure for a domain type, so that we don't have to waste Claude Code tokens.
COMPLETED Add postgres listener code
It is not very clear how events will be distributed into the client but we should at least be able to implement a simple listener and get it to work with patched sqlgen.
COMPLETED Add a logout message code
At present we are just closing the socket. We should send a proper message. The server should close the connection when receiving this message.
COMPLETED Create faker for past timepoint code
We have helper code in currency that needs to be moved to utility:
std::string fake_datetime_string() { // Define range: e.g., years 1970 to 2038 (avoid 9999 unless needed) using namespace std::chrono; static thread_local std::mt19937_64 rng{std::random_device{}()}; // Unix time range: 0 = 1970-01-01, max ~2106 for 32-bit, but we use 64-bit const auto min_time = sys_days{year{1970}/1/1}.time_since_epoch(); const auto max_time = sys_days{year{2038}/12/31}.time_since_epoch() + 24h - 1s; std::uniform_int_distribution<std::int64_t> dist( min_time.count(), max_time.count() ); auto tp = sys_seconds{seconds{dist(rng)}}; // Format as "YYYY-MM-DD HH:MM:SS" return std::format("{:%Y-%m-%d %H:%M:%S}", tp); }
Use this code in eventing.
COMPLETED Notification of server restarts code
Rationale: implemented with heartbeats.
Whenever we lose connection to the server the UI needs to tell the user and change to disconnected.
CANCELLED Fix gemini cli action code
Rationale: the review agent is good enough.
The action to review PRs using gemini is failing.
COMPLETED Implement the event bus code
Implement an in-process typed pub/sub event bus that decouples event sources (e.g., PostgreSQL listener) from event sinks (e.g., subscription manager, UI controllers). The event bus is the foundation for the notification system, enabling components to publish and subscribe to domain events without direct coupling.
The design follows the principle that most components are either event sources (one-way: external → bus) or event sinks (one-way: bus → external). Only the client-side socket bridge qualifies as a true adapter (bidirectional with subscription protocol management).
- Architecture
- event_bus: Pure in-process typed pub/sub mechanism in
ores.eventing. - Event sources: Components that observe external state and publish to the bus
(e.g.,
postgres_event_sourcewrappingpostgres_listener_service). - Event sinks: Components that subscribe and perform side effects (e.g.,
subscription_managerforwarding to sessions). - Domain events: Each component owns its events (e.g.,
ores::risk::domain::events::currency_changed_event).
- event_bus: Pure in-process typed pub/sub mechanism in
- Tasks
- Phase 1: Core Event Bus
[X]Createevent_busclass inores.eventing/service- Thread-safe publish/subscribe
- Type-erased internal storage with typed API
- RAII subscription handles
[X]Createsubscriptionclass for lifetime management[X]Add unit tests for event_bus
- Phase 2: Domain Events
[X]Createeventsnamespace inores.risk/domainwithcurrency_changed_event[X]Createeventsnamespace inores.accounts/domainwithaccount_changed_event[X]Defineevent_traitstemplate for event type → string mapping
- Phase 3: Event Source Integration
[X]Createpostgres_event_sourceclass- Wraps
postgres_listener_service - Maps
entity_change_eventto typed domain events - Publishes to event_bus
- Wraps
[X]Add registrar pattern for event source registration[X]Wire postgres_event_source in service startup
- Phase 4: Protocol Messages
[X]Add notification message types tomessage_types.hppsubscribe_request(0x0010)subscribe_response(0x0011)unsubscribe_request(0x0012)unsubscribe_response(0x0013)notification(0x0014)
[X]Create serialization for subscribe/notification payloads
- Phase 5: Server-Side Subscription Management
[X]Createsubscription_managerservice- Tracks (event_type, key) → set of session_ids
- Thread-safe subscribe/unsubscribe
- Session cleanup on disconnect
[X]Create notification message handler (subscription_handler)[X]Subscribe to event_bus and forward to interested sessions[X]Wire subscription_manager in service startup[X]Extend session to support server-push notifications[X]Add tests for subscription functionality[X]Update UML diagrams
- Phase 6: Client Integration
[X]Handle notification frames in clientrun_message_loop()[X]Addnotification_callback_tto client[X]Createremote_event_adapterfor client-side subscriptions- Maps local subscribe → SUBSCRIBE protocol message
- Maps incoming NOTIFICATION → callback invocation
- Supports re-subscription after reconnect
- Phase 7: Qt Integration
[X]AddnotificationReceivedsignal toClientManager[X]Wire notification callback to Qt signal[X]UpdateCurrencyControllerto subscribe to currency changes[X]Add visual indicator for stale data inCurrencyMdiWindow
- Phase 1: Core Event Bus
COMPLETED Add a version attribute to entities code
At present we are relying only on bitemporal for version control. However it is much easier to check a version number. It should just be a linear attribute which is updated whenever we insert into the entity table. If the user attempts to update an entity but the current version is smaller than the latest version we should error. Make version unique.
Notes:
- add command to cli to create profile entry.
- can create accounts when not logged in from shell, even when not in bootstrap mode.
- accounts have dodgy passwords, even with check on.
- account ID that already exists results in a low-level error:
ores-shell> ✗ Error: Repository error: Executing INSERT failed: ERROR: duplicate key value violates unique constraint "accounts_username_unique_idx" DETAIL: Key (username)=(newuser24) already exists.
We should have a proper application exception for it.
COMPLETED Retest all recipes after latest changes code
We have done a lot of changes across the board and we need to make sure we did not break any of the existing functionality. Start with a clean database, create accounts, import currencies etc. - exercise all functionality and make sure it all works as expected.
Notes:
- Created a new account but cannot login. Error: "Login failed: Login tracking information missing". Create account should have handled this? We are doing a raw write to repo.
- can we notify/listen from shell? should be possible, just tell user of news on next command.
- "✗ Not logged in. Cannot logout." if not logged in, don't try to logout.
COMPLETED Server must check database connectivity on startup code
At present the server only checks connectivity to database when a user requests an operation. We need some kind of initial polling which puts the server in a mode replying to all clients: "database not available". While this is happening, it should keep polling the database and checking the connectivity.
Ideally the server should start, but poll the DB. It should send messages to clients informing them of the issues with database.
Notes:
- health monitor looks like a service and should really live in service folder.
- remove log helper in database.
COMPLETED Add system tray support code
We need to be able to show notifications to users in the system tray - or whatever is the idiomatic way of doing this.
Links:
COMPLETED Red build after account lock PR code
We rushed the PR last night and left a broken build.
COMPLETED Valgrind is detecting issues with comms tests code
==51184== at 0x566B796: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Identity, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >::size() const (hashtable.h:648)
==51184== by 0x566989D: std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::size() const (unordered_set.h:312)
==51184== by 0x5665663: ores::comms::service::subscription_manager::unsubscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_manager.cpp:141)
==51184== by 0x565FBD1: ores::comms::service::subscription_handler::handle_unsubscribe_request(std::span<std::byte const, 18446744073709551615ul>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_handler.cpp:109)
==51184== by 0x565D689: ores::comms::service::subscription_handler::handle_message(ores::comms::service::subscription_handler::handle_message(ores::comms::messaging::message_type, std::span<std::byte const, 18446744073709551615ul>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)::_ZN4ores5comms7service20subscription_handler14handle_messageENS0_9messaging12message_typeESt4spanIKSt4byteLm18446744073709551615EERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE.Frame*) [clone .actor] (subscription_handler.cpp:47)
==51184== by 0x2D66BA: std::__n4861::coroutine_handle<void>::resume() const (coroutine:135)
==51184== by 0x300BA6: boost::asio::detail::awaitable_frame_base<boost::asio::any_io_executor>::resume() (awaitable.hpp:506)
==51184== by 0x2FED02: boost::asio::detail::awaitable_thread<boost::asio::any_io_executor>::pump() (awaitable.hpp:774)
==51184== by 0x30A2B8: boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void>::operator()() (awaitable.hpp:813)
==51184== by 0x309BA9: boost::asio::detail::binder0<boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void> >::operator()() (bind_handler.hpp:56)
==51184== by 0x309F01: void boost::asio::detail::executor_function::complete<boost::asio::detail::binder0<boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void> >, std::allocator<void> >(boost::asio::detail::executor_function::impl_base*, bool) (executor_function.hpp:113)
==51184== by 0x2D7E50: boost::asio::detail::executor_function::operator()() (executor_function.hpp:61)
==51184== by 0x3031C2: boost::asio::detail::executor_op<boost::asio::detail::executor_function, std::allocator<void>, boost::asio::detail::scheduler_operation>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) (executor_op.hpp:70)
==51184== by 0x2DCB33: boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) (scheduler_operation.hpp:40)
==51184== by 0x2E3E90: boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) (scheduler.ipp:501)
==51184== by 0x2E3093: boost::asio::detail::scheduler::run(boost::system::error_code&) (scheduler.ipp:217)
==51184== by 0x2E49D4: boost::asio::io_context::run() (io_context.ipp:63)
==51184== by 0x38BC64: void ores::testing::run_coroutine_test<(anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}>(boost::asio::io_context&, (anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}&&) (run_coroutine_test.hpp:37)
==51184== by 0x3871BF: (anonymous namespace)::CATCH2_INTERNAL_TEST_28() (subscription_protocol_tests.cpp:266)
==51184== by 0x4108ED: Catch::(anonymous namespace)::TestInvokerAsFunction::invoke() const (catch_test_registry.cpp:60)
==51184== by 0x3F9EE6: Catch::TestCaseHandle::invoke() const (catch_test_case_info.hpp:124)
==51184== by 0x3F877D: Catch::RunContext::invokeActiveTestCase() (catch_run_context.cpp:661)
==51184== by 0x3F846C: Catch::RunContext::runCurrentTest() (catch_run_context.cpp:619)
==51184== by 0x3F653F: Catch::RunContext::runTest(Catch::TestCaseHandle const&) (catch_run_context.cpp:282)
==51184== by 0x3A0488: Catch::(anonymous namespace)::TestGroup::execute() (catch_session.cpp:110)
==51184== by 0x3A1D86: Catch::Session::runInternal() (catch_session.cpp:379)
==51184== by 0x3A186B: Catch::Session::run() (catch_session.cpp:306)
==51184== by 0x299610: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==51184== by 0x297F89: main (main.cpp:28)
==51184== Address 0x9024650 is 64 bytes inside a block of size 104 free'd
==51184== at 0x484A61D: operator delete(void*, unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==51184== by 0x379445: std::__new_allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> >::deallocate(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*, unsigned long) (new_allocator.h:172)
==51184== by 0x378C3F: deallocate (allocator.h:210)
==51184== by 0x378C3F: deallocate (alloc_traits.h:517)
==51184== by 0x378C3F: std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >::_M_deallocate_node_ptr(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*) (hashtable_policy.h:2022)
==51184== by 0x378792: std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >::_M_deallocate_node(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*) (hashtable_policy.h:2012)
==51184== by 0x5670FFF: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_erase(unsigned long, std::__detail::_Hash_node_base*, std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*) (hashtable.h:2353)
==51184== by 0x566E1D9: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::erase(std::__detail::_Node_const_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, false, true>) (hashtable.h:2328)
==51184== by 0x566B70A: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::erase(std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, false, true>) (hashtable.h:980)
==51184== by 0x5669854: std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > >::erase(std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, false, true>) (unordered_map.h:753)
==51184== by 0x56655B2: ores::comms::service::subscription_manager::unsubscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_manager.cpp:136)
==51184== by 0x565FBD1: ores::comms::service::subscription_handler::handle_unsubscribe_request(std::span<std::byte const, 18446744073709551615ul>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_handler.cpp:109)
==51184== by 0x565D689: ores::comms::service::subscription_handler::handle_message(ores::comms::service::subscription_handler::handle_message(ores::comms::messaging::message_type, std::span<std::byte const, 18446744073709551615ul>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)::_ZN4ores5comms7service20subscription_handler14handle_messageENS0_9messaging12message_typeESt4spanIKSt4byteLm18446744073709551615EERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE.Frame*) [clone .actor] (subscription_handler.cpp:47)
==51184== by 0x2D66BA: std::__n4861::coroutine_handle<void>::resume() const (coroutine:135)
==51184== by 0x300BA6: boost::asio::detail::awaitable_frame_base<boost::asio::any_io_executor>::resume() (awaitable.hpp:506)
==51184== by 0x2FED02: boost::asio::detail::awaitable_thread<boost::asio::any_io_executor>::pump() (awaitable.hpp:774)
==51184== by 0x30A2B8: boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void>::operator()() (awaitable.hpp:813)
==51184== by 0x309BA9: boost::asio::detail::binder0<boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void> >::operator()() (bind_handler.hpp:56)
==51184== by 0x309F01: void boost::asio::detail::executor_function::complete<boost::asio::detail::binder0<boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void> >, std::allocator<void> >(boost::asio::detail::executor_function::impl_base*, bool) (executor_function.hpp:113)
==51184== by 0x2D7E50: boost::asio::detail::executor_function::operator()() (executor_function.hpp:61)
==51184== by 0x3031C2: boost::asio::detail::executor_op<boost::asio::detail::executor_function, std::allocator<void>, boost::asio::detail::scheduler_operation>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) (executor_op.hpp:70)
==51184== by 0x2DCB33: boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) (scheduler_operation.hpp:40)
==51184== by 0x2E3E90: boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) (scheduler.ipp:501)
==51184== by 0x2E3093: boost::asio::detail::scheduler::run(boost::system::error_code&) (scheduler.ipp:217)
==51184== by 0x2E49D4: boost::asio::io_context::run() (io_context.ipp:63)
==51184== by 0x38BC64: void ores::testing::run_coroutine_test<(anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}>(boost::asio::io_context&, (anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}&&) (run_coroutine_test.hpp:37)
==51184== by 0x3871BF: (anonymous namespace)::CATCH2_INTERNAL_TEST_28() (subscription_protocol_tests.cpp:266)
==51184== by 0x4108ED: Catch::(anonymous namespace)::TestInvokerAsFunction::invoke() const (catch_test_registry.cpp:60)
==51184== by 0x3F9EE6: Catch::TestCaseHandle::invoke() const (catch_test_case_info.hpp:124)
==51184== by 0x3F877D: Catch::RunContext::invokeActiveTestCase() (catch_run_context.cpp:661)
==51184== by 0x3F846C: Catch::RunContext::runCurrentTest() (catch_run_context.cpp:619)
==51184== by 0x3F653F: Catch::RunContext::runTest(Catch::TestCaseHandle const&) (catch_run_context.cpp:282)
==51184== by 0x3A0488: Catch::(anonymous namespace)::TestGroup::execute() (catch_session.cpp:110)
==51184== by 0x3A1D86: Catch::Session::runInternal() (catch_session.cpp:379)
==51184== by 0x3A186B: Catch::Session::run() (catch_session.cpp:306)
==51184== by 0x299610: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==51184== by 0x297F89: main (main.cpp:28)
==51184== Block was alloc'd at
==51184== at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==51184== by 0x5675F99: std::__new_allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> >::allocate(unsigned long, void const*) (new_allocator.h:151)
==51184== by 0x567172C: allocate (allocator.h:198)
==51184== by 0x567172C: allocate (alloc_traits.h:482)
==51184== by 0x567172C: std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>* std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (hashtable_policy.h:1990)
==51184== by 0x566E502: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_Scoped_node::_Scoped_node<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >*, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (hashtable.h:307)
==51184== by 0x566BB02: std::__detail::_Map_base<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true>, true>::operator[](std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (hashtable_policy.h:818)
==51184== by 0x5669994: std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > >::operator[](std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (unordered_map.h:987)
==51184== by 0x566420F: ores::comms::service::subscription_manager::subscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_manager.cpp:98)
==51184== by 0x3852FB: (anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}::operator()((anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}::operator()() const::_ZZN12_GLOBAL__N_1L23CATCH2_INTERNAL_TEST_28EvENKUlvE_clEv.Frame*) [clone .actor] (subscription_protocol_tests.cpp:274)
==51184== by 0x2D66BA: std::__n4861::coroutine_handle<void>::resume() const (coroutine:135)
==51184== by 0x300BA6: boost::asio::detail::awaitable_frame_base<boost::asio::any_io_executor>::resume() (awaitable.hpp:506)
==51184== by 0x2FED02: boost::asio::detail::awaitable_thread<boost::asio::any_io_executor>::pump() (awaitable.hpp:774)
==51184== by 0x30A2B8: boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void>::operator()() (awaitable.hpp:813)
==51184== by 0x309BA9: boost::asio::detail::binder0<boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void> >::operator()() (bind_handler.hpp:56)
==51184== by 0x309F01: void boost::asio::detail::executor_function::complete<boost::asio::detail::binder0<boost::asio::detail::awaitable_async_op_handler<void (), boost::asio::any_io_executor, void> >, std::allocator<void> >(boost::asio::detail::executor_function::impl_base*, bool) (executor_function.hpp:113)
==51184== by 0x2D7E50: boost::asio::detail::executor_function::operator()() (executor_function.hpp:61)
==51184== by 0x3031C2: boost::asio::detail::executor_op<boost::asio::detail::executor_function, std::allocator<void>, boost::asio::detail::scheduler_operation>::do_complete(void*, boost::asio::detail::scheduler_operation*, boost::system::error_code const&, unsigned long) (executor_op.hpp:70)
==51184== by 0x2DCB33: boost::asio::detail::scheduler_operation::complete(void*, boost::system::error_code const&, unsigned long) (scheduler_operation.hpp:40)
==51184== by 0x2E3E90: boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) (scheduler.ipp:501)
==51184== by 0x2E3093: boost::asio::detail::scheduler::run(boost::system::error_code&) (scheduler.ipp:217)
==51184== by 0x2E49D4: boost::asio::io_context::run() (io_context.ipp:63)
==51184== by 0x38BC64: void ores::testing::run_coroutine_test<(anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}>(boost::asio::io_context&, (anonymous namespace)::CATCH2_INTERNAL_TEST_28()::{lambda()#1}&&) (run_coroutine_test.hpp:37)
==51184== by 0x3871BF: (anonymous namespace)::CATCH2_INTERNAL_TEST_28() (subscription_protocol_tests.cpp:266)
==51184== by 0x4108ED: Catch::(anonymous namespace)::TestInvokerAsFunction::invoke() const (catch_test_registry.cpp:60)
==51184== by 0x3F9EE6: Catch::TestCaseHandle::invoke() const (catch_test_case_info.hpp:124)
==51184== by 0x3F877D: Catch::RunContext::invokeActiveTestCase() (catch_run_context.cpp:661)
==51184== by 0x3F846C: Catch::RunContext::runCurrentTest() (catch_run_context.cpp:619)
==51184== by 0x3F653F: Catch::RunContext::runTest(Catch::TestCaseHandle const&) (catch_run_context.cpp:282)
==51184== by 0x3A0488: Catch::(anonymous namespace)::TestGroup::execute() (catch_session.cpp:110)
==51184== by 0x3A1D86: Catch::Session::runInternal() (catch_session.cpp:379)
==51184== by 0x3A186B: Catch::Session::run() (catch_session.cpp:306)
==51184== by 0x299610: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==51184== by 0x297F89: main (main.cpp:28)
==51184==
<b>UMR</b> ==51184== Invalid read of size 8
==51184== at 0x566B796: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Identity, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >::size() const (hashtable.h:648)
==51184== by 0x566989D: std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >::size() const (unordered_set.h:312)
==51184== by 0x5665663: ores::comms::service::subscription_manager::unsubscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_manager.cpp:141)
==51184== by 0x35F482: (anonymous namespace)::CATCH2_INTERNAL_TEST_8() (subscription_manager_tests.cpp:76)
==51184== by 0x4108ED: Catch::(anonymous namespace)::TestInvokerAsFunction::invoke() const (catch_test_registry.cpp:60)
==51184== by 0x3F9EE6: Catch::TestCaseHandle::invoke() const (catch_test_case_info.hpp:124)
==51184== by 0x3F877D: Catch::RunContext::invokeActiveTestCase() (catch_run_context.cpp:661)
==51184== by 0x3F846C: Catch::RunContext::runCurrentTest() (catch_run_context.cpp:619)
==51184== by 0x3F653F: Catch::RunContext::runTest(Catch::TestCaseHandle const&) (catch_run_context.cpp:282)
==51184== by 0x3A0488: Catch::(anonymous namespace)::TestGroup::execute() (catch_session.cpp:110)
==51184== by 0x3A1D86: Catch::Session::runInternal() (catch_session.cpp:379)
==51184== by 0x3A186B: Catch::Session::run() (catch_session.cpp:306)
==51184== by 0x299610: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==51184== by 0x297F89: main (main.cpp:28)
==51184== Address 0xa7418b0 is 64 bytes inside a block of size 104 free'd
==51184== at 0x484A61D: operator delete(void*, unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==51184== by 0x379445: std::__new_allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> >::deallocate(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*, unsigned long) (new_allocator.h:172)
==51184== by 0x378C3F: deallocate (allocator.h:210)
==51184== by 0x378C3F: deallocate (alloc_traits.h:517)
==51184== by 0x378C3F: std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >::_M_deallocate_node_ptr(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*) (hashtable_policy.h:2022)
==51184== by 0x378792: std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >::_M_deallocate_node(std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*) (hashtable_policy.h:2012)
==51184== by 0x5670FFF: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_erase(unsigned long, std::__detail::_Hash_node_base*, std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>*) (hashtable.h:2353)
==51184== by 0x566E1D9: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::erase(std::__detail::_Node_const_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, false, true>) (hashtable.h:2328)
==51184== by 0x566B70A: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::erase(std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, false, true>) (hashtable.h:980)
==51184== by 0x5669854: std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > >::erase(std::__detail::_Node_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, false, true>) (unordered_map.h:753)
==51184== by 0x56655B2: ores::comms::service::subscription_manager::unsubscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_manager.cpp:136)
==51184== by 0x35F482: (anonymous namespace)::CATCH2_INTERNAL_TEST_8() (subscription_manager_tests.cpp:76)
==51184== by 0x4108ED: Catch::(anonymous namespace)::TestInvokerAsFunction::invoke() const (catch_test_registry.cpp:60)
==51184== by 0x3F9EE6: Catch::TestCaseHandle::invoke() const (catch_test_case_info.hpp:124)
==51184== by 0x3F877D: Catch::RunContext::invokeActiveTestCase() (catch_run_context.cpp:661)
==51184== by 0x3F846C: Catch::RunContext::runCurrentTest() (catch_run_context.cpp:619)
==51184== by 0x3F653F: Catch::RunContext::runTest(Catch::TestCaseHandle const&) (catch_run_context.cpp:282)
==51184== by 0x3A0488: Catch::(anonymous namespace)::TestGroup::execute() (catch_session.cpp:110)
==51184== by 0x3A1D86: Catch::Session::runInternal() (catch_session.cpp:379)
==51184== by 0x3A186B: Catch::Session::run() (catch_session.cpp:306)
==51184== by 0x299610: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==51184== by 0x297F89: main (main.cpp:28)
==51184== Block was alloc'd at
==51184== at 0x4846FA3: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==51184== by 0x5675F99: std::__new_allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> >::allocate(unsigned long, void const*) (new_allocator.h:151)
==51184== by 0x567172C: allocate (allocator.h:198)
==51184== by 0x567172C: allocate (alloc_traits.h:482)
==51184== by 0x567172C: std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true>* std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (hashtable_policy.h:1990)
==51184== by 0x566E502: std::_Hashtable<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_Scoped_node::_Scoped_node<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, true> > >*, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (hashtable.h:307)
==51184== by 0x566BB02: std::__detail::_Map_base<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >, std::__detail::_Select1st, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true>, true>::operator[](std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (hashtable_policy.h:818)
==51184== by 0x5669994: std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::unordered_set<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > >::operator[](std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (unordered_map.h:987)
==51184== by 0x566420F: ores::comms::service::subscription_manager::subscribe(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (subscription_manager.cpp:98)
==51184== by 0x35F2C8: (anonymous namespace)::CATCH2_INTERNAL_TEST_8() (subscription_manager_tests.cpp:74)
==51184== by 0x4108ED: Catch::(anonymous namespace)::TestInvokerAsFunction::invoke() const (catch_test_registry.cpp:60)
==51184== by 0x3F9EE6: Catch::TestCaseHandle::invoke() const (catch_test_case_info.hpp:124)
==51184== by 0x3F877D: Catch::RunContext::invokeActiveTestCase() (catch_run_context.cpp:661)
==51184== by 0x3F846C: Catch::RunContext::runCurrentTest() (catch_run_context.cpp:619)
==51184== by 0x3F653F: Catch::RunContext::runTest(Catch::TestCaseHandle const&) (catch_run_context.cpp:282)
==51184== by 0x3A0488: Catch::(anonymous namespace)::TestGroup::execute() (catch_session.cpp:110)
==51184== by 0x3A1D86: Catch::Session::runInternal() (catch_session.cpp:379)
==51184== by 0x3A186B: Catch::Session::run() (catch_session.cpp:306)
==51184== by 0x299610: int Catch::Session::run<char>(int, char const* const*) (catch_session.hpp:49)
==51184== by 0x297F89: main (main.cpp:28)
==51184==
COMPLETED Saving a new currency should close window code
At present when we enter a new currency and press save the currency stays there.
Not auto reloading:
My opinion on reload behavior:
I'd recommend the yellow button indicator approach for these reasons:
- User control - The user stays in control of when the list refreshes, which respects their workflow. They might be in the middle of looking at something else in the list.
- Preserves state - Auto-reload could disrupt scroll position, selection, or sorting the user had set up.
- Consistent pattern - This is a common pattern in professional applications (e.g., email clients showing "new messages" indicator rather than auto-scrolling). Users understand "yellow = stale data, click to refresh."
- Multi-user scenarios - If multiple users are working, auto-reload on every change could be disruptive. The indicator lets users refresh when convenient.
- Predictability - Auto-reload can feel jarring or "magical." The indicator is explicit about what happened.
The one exception might be: if the user created a currency and the Currencies list window is not currently visible/focused, then auto-reload is harmless since they won't notice the refresh anyway. But for consistency, I'd keep the yellow indicator approach everywhere.
COMPLETED Add system.disable_password_validation to manifest code
This flag was dropped with all of the refactorings.
COMPLETED Consider adding top-level login and logout commands in shell code
Rationale: Implemented.
At present we need to do:
accounts login accounts logout
We could have aliases to make this a bit more obvious:
login logout
COMPLETED Retry logic does not notify give-up code
At present we log:
2025-12-13 04:02:39.718163 [INFO] [ores.comms.net.client] Retrying reconnection in 3755ms 2025-12-13 04:02:43.473265 [INFO] [ores.comms.net.client] Reconnection attempt 4 of 5 2025-12-13 04:02:43.473359 [INFO] [ores.comms.net.client] Connecting to localhost:55555 2025-12-13 04:02:43.473751 [WARN] [ores.comms.net.client] Reconnection attempt 4 failed: Connection refused [system:111 at /home/marco/Development/OreStudio/OreStudio.local1/build/output/linux-clang-debug/vcpkg_installed/x64-linux/include/boost/asio/detail/reactive_socket_connect_op.hpp:97:37 in function 'static void boost::asio::detail::reactive_socket_connect_op<boost::asio::detail::range_connect_op<boost::asio::ip::tcp, boost::asio::any_io_executor, boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>, boost::asio::detail::default_connect_condition, boost::asio::detail::awaitable_handler<boost::asio::any_io_executor, boost::system::error_code, boost::asio::ip::basic_endpoint<boost::asio::ip::tcp>>>, boost::asio::any_io_executor>::do_complete(void *, operation *, const boost::system::error_code &, std::size_t) [Handler = boost::asio::detail::range_connect_op<boost::asio::ip::tcp, boost::asio::any_io_executor, boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>, boost::asio::detail::default_connect_condition, boost::asio::detail::awaitable_handler<boost::asio::any_io_executor, boost::system::error_code, boost::asio::ip::basic_endpoint<boost::asio::ip::tcp>>>, IoExecutor = boost::asio::any_io_executor]'] 2025-12-13 04:02:43.473798 [INFO] [ores.comms.net.client] Retrying reconnection in 9494ms
But nothing after that.
Footer
| Previous: Version Zero |