ravenscott-blog/markdown/API Caching for Minecraft Servers.md
2024-09-16 07:53:26 -04:00

185 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- lead -->
Finding an efficient way to cache data into memory.
# Building a Fast and Efficient Minecraft Server Cache Manager in Node.js
In this article, we will take an in-depth look at how to build a cache manager in Node.js to drastically improve the performance of your application, specifically for listing Minecraft servers. Without caching, the response time for listing all the servers was over 2 minutes due to multiple heavy computations and I/O operations. With the introduction of a caching mechanism, this time was reduced to an impressive 2 milliseconds. Lets dive into the code and understand how this was achieved.
## Overview of the System
The purpose of this system is to manage a list of Minecraft servers efficiently. The core functionality includes fetching server information, such as the server's MOTD (Message of the Day), online status, game version, and operational details (e.g., ops, whitelist, banned players). The data is sourced from Docker containers, local file systems, and remote authentication services.
### Problem Statement
When youre dealing with numerous Minecraft servers, querying real-time server data can become very expensive in terms of performance. Each query requires:
- Accessing container information via Docker.
- Checking server online status via network requests.
- Fetching MOTD and other server metadata.
- Interacting with the file system to fetch or generate tokens.
These operations, especially when scaled to multiple servers, can significantly slow down the response time. Without caching, the requests took over 2 minutes to process. This system mitigates those delays by introducing an efficient in-memory cache.
## The Caching Mechanism
The system employs a caching layer that stores various pieces of server information (MOTD, online status, etc.) in memory. This avoids repeated heavy I/O and network operations, thus drastically reducing the request time.
### Key Components of the Cache
1. **MOTD Cache:** Stores the Message of the Day for each server.
2. **Online Status Cache:** Stores whether a server is online or offline.
3. **Token Cache:** Stores authentication tokens for server owners.
4. **Container Info Cache:** Caches Docker container details for each server.
5. **Ops, Whitelist, Banned Players Caches:** Caches server-specific administrative data.
These caches are refreshed periodically and stored in an in-memory object, reducing the need for redundant calls to external systems.
## Code Walkthrough: The Cache Manager
### Directory and File Structure
- **Cache Directory:** The `cacheDir` (in this case, `/home/cache`) stores JSON files that provide metadata for each Minecraft server. Each file corresponds to a specific server and includes connection details and server-specific configuration data.
```javascript
const cacheDir = "/home/cache";
const cache = {
motd: {},
online: {},
token: {},
containerInfo: {},
ops: {},
whitelist: {},
whitelistEnable: {},
banned: {}
};
```
The `cache` object holds the in-memory data for all servers, avoiding the need to repeatedly fetch this data from external sources.
### Server Listing Function
Heres the code for listing all servers, including how caching is used to optimize performance:
```javascript
exports.list_all_servers = async function (req, res) {
let secret_key = req.params["secret_key"];
if (secret_key !== config.secret_key) {
return res.status(401).json({ success: false, error: "Unauthorized." });
}
try {
const files = await fs.promises.readdir(cacheDir);
let jumpnode_servers = [];
for (const file of files) {
const filePath = path.join(cacheDir, file);
const data = await jsonfile.readFile(filePath);
const serverName = path.basename(file, path.extname(file)).replace('.mc', '');
// Skip certain servers
if (serverName.includes(".link")) {
continue;
}
// Fetch data from cache or fallback to fetching from the source
const connectString = data.connect;
const motd = cache.motd[connectString]?.value ?? null;
const online = cache.online[connectString]?.value ?? false;
const token = cache.token[serverName]?.value ?? null;
const containerInfo = cache.containerInfo[serverName]?.value ?? null;
const ops = cache.ops[serverName]?.value ?? null;
const whitelist = cache.whitelist[serverName]?.value ?? null;
const whitelistEnable = cache.whitelistEnable[serverName]?.value ?? null;
const banned = cache.banned[serverName]?.value ?? null;
if (!containerInfo) {
continue; // Skip servers with no container info
}
// Server information is stored in jumpnode_servers array
const serverInfo = {
serverName,
gameVersion: containerInfo.Config.Image.split(':').pop(),
connect: data.connect,
ownersToken: token,
ops,
whitelist,
whitelistEnable,
banlist: banned,
motd,
online
};
jumpnode_servers.push(serverInfo);
}
return res.json({ success: true, servers: jumpnode_servers });
} catch (err) {
console.error("Error reading cache directory:", err);
return res.status(500).json({ success: false, error: err.message });
}
};
```
This function first checks the secret key for authentication. If the secret key is valid, it proceeds to list all servers. The server data is retrieved from the in-memory cache, avoiding the need to re-fetch the data from slow external sources like Docker, network connections, and the file system.
### Caching Core Functions
#### Fetching MOTD
The function `fetchMOTD` sends a custom status request packet to the server and waits for its response. The result is cached, so future requests can serve the MOTD immediately without querying the server again.
```javascript
async function fetchMOTD(connectString) {
const client = new net.Socket();
const serverPort = connectString.split(':')[1];
// Sends the packet to get the MOTD
client.connect(serverPort, 'my-mc.link', () => {
client.write(Buffer.from([0xFE, 0x01]));
});
// Process and cache the response
client.on('data', (data) => {
const response = data.toString('utf8').split('\x00\x00\x00');
if (response.length >= 6) {
const motd = response[3].replace(/\u0000/g, '');
cache.motd[connectString] = { value: motd, timestamp: Date.now() };
}
client.destroy();
});
}
```
#### Fetching Container Info
The `fetchContainerInfo` function interacts with Docker to fetch container details for each Minecraft server. Dockerode is used to communicate with the Docker API. The result is cached in memory to avoid repeatedly querying Docker for container data.
```javascript
async function fetchContainerInfo(serverName) {
const container = docker.getContainer(serverName);
const containerInfo = await container.inspect();
cache.containerInfo[serverName] = { value: containerInfo, timestamp: Date.now() };
}
```
#### Other Cache Functions
Similar functions exist for fetching online status, tokens, ops, whitelist, and banned player lists, all of which follow the same caching pattern.
## Cache Update Strategy
The cache is updated by periodically calling the `updateCache` function, which refreshes the cache every 60 seconds:
```javascript
setInterval(updateCache, 60000);
```
This ensures that the cache is kept relatively up-to-date while still providing the performance benefits of avoiding repeated heavy I/O operations. The cache holds both the value and a timestamp, allowing future improvements like time-based cache invalidation if needed.
## Conclusion
By introducing an in-memory caching system, this application was able to reduce request times from over 2 minutes to just 2 milliseconds. This system efficiently caches key server data, eliminating the need to repeatedly query external services like Docker, network services, and file systems. This approach is especially useful in applications with high I/O overhead and network latency, allowing for faster and more responsive interactions.
This caching strategy could be adapted and extended further, for instance by adding cache expiration policies or integrating Redis for distributed caching in a multi-node architecture.