-
Notifications
You must be signed in to change notification settings - Fork 2
Redis
Redis (Remote Dictionary Server) is an open-source, in-memory data structure store. It is commonly used as:
- A cache
- A session store
- A message broker
- A lightweight NoSQL database
Unlike traditional databases that read and write primarily to disk, Redis stores data in memory, which makes it extremely fast (sub-millisecond response times).
In our application, Redis is used primarily for caching Running Games State, ensuring quick access. Games in Redis expire automatically after a defined TTL (Time To Live).
Redis runs as a separate service on the server. It is installed and started independently from any dependency, though the gameManager Docker container uses it as a dependency.
In redis.rs we define all logic related to connecting to Redis, managing distributed locks, and storing/retrieving game state.
We use bb8 as an async connection pool manager together with bb8_redis. This ensures we reuse connections efficiently across concurrent requests rather than opening a new connection on every operation.
`redis.rs`
pub async fn create_pool(redis_url: &str) -> RedisPool {
let manager = RedisConnectionManager::new(redis_url)
.expect("Error creating Redis manager");
bb8::Pool::builder()
.build(manager)
.await
.expect("Redis pool cannot be created")
}The RedisPool type alias is defined as:
`redis.rs`
pub type RedisPool = bb8::Pool<RedisConnectionManager>;Since multiple requests could attempt to write to the same game state concurrently, we use a distributed lock pattern via Redis atomic commands.
`redis.rs`
let result: Option<String> = redis::cmd("SET")
.arg(&lock_key)
.arg("locked")
.arg("NX") // Only set if Not eXists
.arg("EX")
.arg(ttl_secs)
.query_async(&mut *conn)
.await
.map_err(MatchError::Redis)?;The NX flag makes this operation atomic: only one caller will receive Some(...) back, meaning they acquired the lock. All others receive None.
Lock keys follow the pattern lock:match:{match_id} and expire automatically via EX to prevent deadlocks if a process crashes mid-operation.
`redis.rs`
pub async fn release_lock(pool: &RedisPool, match_id: &str) -> Result<(), MatchError> {
let mut conn = pool.get().await.map_err(|_| MatchError::Pool)?;
let lock_key = format!("lock:match:{}", match_id);
let _: () = conn.del(lock_key).await.map_err(MatchError::Redis)?;
Ok(())
}The lock is always explicitly released after the write operation completes.
Game state is serialized to JSON and stored under keys of the form match:{match_id}, with a TTL of 3600 seconds (1 hour).
The save_match_state function wraps the write in a retry loop that attempts to acquire the lock up to 10 times, sleeping 50ms between attempts:
`redis.rs`
for _ in 0..10 {
if acquire_lock(pool, match_id, 5).await? {
// ... perform write ...
release_lock(pool, match_id).await?;
return result;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
Err(MatchError::LockTimeout)If the lock cannot be acquired after all retries, a MatchError::LockTimeout is returned.
create_match bootstraps a new game by:
- Building the initial board layout in YEN notation (the game engine's format).
- Serializing it to JSON.
- Persisting both the player list (
match:{id}:players) and the initial game state (match:{id}) to Redis.
`redis.rs`
let layout: String = (1u32..=*size)
.map(|row| ".".repeat(row as usize))
.collect::<Vec<_>>()
.join("/");
let initial_state = YEN::new(*size, 0, vec!['B', 'R'], layout);
let state_json = serde_json::to_string(&initial_state)?;All Redis operations return a Result<_, MatchError>, a custom error type defined with thiserror:
| Variant | Cause |
|---|---|
Redis |
Underlying redis::RedisError
|
Serialization |
serde_json failure during state encoding |
Pool |
Could not obtain a connection from bb8 pool |
LockTimeout |
Lock not acquired after 10 retries |