In-Memory Data Caching
Relevant source files
The following files were used as context for generating this wiki page:
In-memory data caching in the HX-Music project provides high-performance data access by loading all database records into memory during system startup. This architecture ensures that subsequent read operations occur at memory speeds, while write operations (create, update, delete) synchronize with the underlying SQLite database to maintain data persistence.
The system relies on a thread-safe Data Access Object (DAO) pattern to manage this cache. Components like the MusicApi and CoverApi interact with this cache through a centralized MemoryDAOPool, ensuring consistent data views across different server modules.
Sources: include/dao/ThreadSafeInMemoryDAO.hpp:27-32, HX-Music-Server/doc/api.md:65-75
Core Architecture
The caching system uses the ThreadSafeInMemoryDAO template class as its foundation. This class manages an internal std::map that mirrors the structure of a database table.
Components and Data Flow
- Initialization: Upon instantiation, the DAO creates the required database table if it does not exist and performs a
queryAllto populate the internal map. - Read Operations: Read requests access the internal map using shared locks, allowing multiple concurrent readers.
- Write Operations: Write requests (Add/Update/Delete) acquire a unique lock, update the SQLite database first, and then update the internal map to ensure the cache remains valid.
The diagram shows the lifecycle of the in-memory cache from initialization to handling concurrent read/write operations.
Sources: include/dao/ThreadSafeInMemoryDAO.hpp:42-53, include/dao/ThreadSafeInMemoryDAO.hpp:57-63
Thread Safety and Locking Mechanism
The system implements a Read-Write lock pattern using std::shared_mutex. This maximizes performance by preventing readers from blocking each other while ensuring data integrity during modifications.
| Operation | Lock Type | Description |
|---|---|---|
at() / lockSelect() | std::shared_lock | Allows multiple threads to read data simultaneously from the cache. |
add() | std::unique_lock | Blocks all other access while inserting into the DB and updating the map. |
update() / updateBy() | std::unique_lock | Blocks all other access while modifying existing records in the DB and cache. |
del() | std::unique_lock | Blocks all other access while removing records from the DB and cache. |
Sources: include/dao/ThreadSafeInMemoryDAO.hpp:109-122, include/dao/ThreadSafeInMemoryDAO.hpp:135-139
Specialized Lock Helpers
The DAO provides functional wrappers to execute logic within specific lock scopes:
uniqueLock(Lambda&& lambda): Executes the provided lambda function under a unique lock.sharedLock(Lambda&& lambda): Executes the provided lambda function under a shared lock.lockSelect(Lambda&& lambda): Provides a read-only reference to the internal map for complex selection logic without allowing modifications that could cause deadlocks.
Sources: include/dao/ThreadSafeInMemoryDAO.hpp:124-139
DAO Implementation Examples
Different modules extend the base ThreadSafeInMemoryDAO to provide domain-specific caching logic, such as secondary index mapping.
UserDAO with Secondary Mapping
The UserDAO maintains a secondary std::map<std::string, uint64_t> to allow fast lookups by username (atName) in addition to the primary ID lookup.
Sources: HX-Music-Server/include/dao/UserDAO.hpp:33-40
The diagram represents the inheritance and extension of the base in-memory DAO for user-specific operations.
Sources: HX-Music-Server/include/dao/UserDAO.hpp:27-31, include/dao/ThreadSafeInMemoryDAO.hpp:31-38
Management and Access (DAOPool)
The project uses MemoryDAOPool as a singleton-like factory to manage DAO instances. It uses a static local variable within a template function to ensure that only one instance of a specific DAO (bound to a specific database path) exists.
// include/dao/MemoryDAOPool.hpp:31-35
template <typename T, meta::FixedString Path>
static std::shared_ptr<T> get() {
using PathStr = meta::ToCharPack<Path>;
static auto dao = std::make_shared<T>(db::SQLiteDB{PathStr::view()});
return dao;
}
Sources: include/dao/MemoryDAOPool.hpp:29-37
Usage in API Endpoints
Endpoints retrieve the cached data accessors during the registration phase. For example, CoverApi retrieves the MusicDAO to check for cover file suffixes stored in memory before attempting to stream the physical file.
Sources: HX-Music-Server/include/api/CoverApi.hpp:42-43, HX-Music-Server/include/api/MusicApi.hpp:51-52
Data Models and Caching Scope
The following table describes the primary data objects (DO) being cached in memory:
| Data Object | Source ID | Purpose |
|---|---|---|
MusicDO | id (uint64) | Stores song path, name, singers, album, and duration. |
PlaylistDO | id (uint64) | Stores playlist metadata and a list of song ID references. |
UserDO | id (uint64) | Stores credentials, permissions, and login session UUIDs. |
Sources: HX-Music-Server/doc/api.md:65-100, HX-Music-Server/include/dao/UserDAO.hpp:85-88
Conclusion
In-memory data caching is central to HX-Music's server performance. By leveraging C++ template meta-programming and thread-safe shared mutexes, the system provides a robust layer that minimizes disk I/O for frequent operations such as music information retrieval and user authentication. The automatic synchronization with SQLite ensures that performance does not come at the cost of data durability.
Sources: include/dao/ThreadSafeInMemoryDAO.hpp:27-32, HX-Music-Server/doc/api.md:70-75