IodidDOCUMENTATION

persistence/redis.mdx

/doc/persistence/redis

PUBLIC DOCUMENTATION

Persistence — Redis

RedisClient Jedis 5.1.0 pool 32, RedisKey 30+ keys, types STRING/HASH/ZSET/SET, member SHA256 16B score mmr, pipeline operations and circuit breaker.

Sources: src/main/java/com/safjnest/redis/RedisClient.java, src/main/java/com/safjnest/redis/RedisKey.java, src/main/java/com/safjnest/utils/JsonCodec.java.

Redis is only ephemeral cache — no authoritative data. Everything is rebuildable from Mongo (docs/architecture/README.md:15, iodid/architecture.mdx).

RedisClient — Jedis 5.1.0 pool 32

RedisClient (src/main/java/com/safjnest/redis/RedisClient.java:23) — single JedisPool, localhost:6379, CONNECTION_TIMEOUT_MS=500 (:28).

java
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(32);        // :36
config.setMaxIdle(32);         // :37
config.setMinIdle(8);          // :38
config.setTestOnBorrow(false); // :39
config.setTestOnReturn(false); // :40
config.setTestWhileIdle(true); // :41
config.setMinEvictableIdleDuration(Duration.ofMinutes(1)); // :42
config.setTimeBetweenEvictionRuns(Duration.ofSeconds(30)); // :43
config.setBlockWhenExhausted(true); // :45
pool = new JedisPool(config, "localhost", 6379, CONNECTION_TIMEOUT_MS);
for (int i=0;i<8;i++) warmup.add(pool.getResource()); // :51
  • SORTED_SET_BATCH_SIZE = 250 (:27) — batch for zadd pipeline.
  • TEMPORARY_TTL_SECONDS = 60 (:29) — every set uses 60s, regardless of RedisKey.ttl.
  • RETRY_AFTER_FAILURE_MS = 30_000 (:30) — circuit breaker.

All methods catch Exception, call markUnavailable() (sets disabledUntil = now+30s :550) and return null/false/0 — never propagate to the caller. canUseRedis() (:545) checks currentTimeMillis >= disabledUntil.

Codec: JsonCodec.toJson/fromJson (JsonCodec.java:28) with FIELD ANY, JavaTimeModule.

RedisKey enum — 30+ keys

RedisKey (src/main/java/com/safjnest/redis/RedisKey.java:7):

java
public enum RedisKey { R4J_SUMMONER("r4j:summoner:by-id:%s:%s", Duration.ofSeconds(60)), ... }
RedisKey(String pattern, Duration ttl) { this.pattern = pattern; this.ttl = ttl; this.prefix = App.isTesting() ? "beebot_test:lol" : "beebot:lol"; }
public String of(Object... args) { return String.format(prefix + ":" + pattern, args); }
public int ttlSeconds() { return (int) ttl.toSeconds(); }

Prefix: beebot:lol prod, beebot_test:lol test (:67). ttlSeconds() returns the declared seconds, but RedisClient.set always uses TEMPORARY_TTL_SECONDS=60.

Complete table

#KeyPatternDeclared TTLEffective TTLTypeValue
1R4J_SUMMONERr4j:summoner:by-id:%s:%s (region, summonerId)60s60sSTRING (JSON)Summoner R4J
2R4J_SUMMONER_IDr4j:summoner:id-by-puuid:%s:%s60s60sSTRINGsummoner ID
3R4J_ACCOUNTr4j:account:by-puuid:%s:%s60s60sSTRINGaccount
4R4J_ACCOUNT_BY_NAMEr4j:account:by-name:%s:%s:%s60s60sSTRINGaccount by gameName#tagLine
5R4J_USER_ID_BY_PUUIDr4j:account:user-id-by-puuid:%s:%s60s (orig 6h)60sSTRINGuserId
6R4J_LEAGUE_ENTRIESr4j:league:entries:%s:%s60s (orig 6h)60sSTRINGleague entries
7R4J_CHAMPION_MASTERIESr4j:league:champion-masteries:%s:%s60s (orig 6h)60sSTRINGmasteries
8R4J_SPECTATOR_CURRENTr4j:match:current:%s:%s60s60sSTRINGLiveGame
9R4J_MATCH_LISTr4j:match:list:%s:%s:%s:%s60s (orig 1h)60sSTRINGmatch list
10R4J_MATCHr4j:match:by-id:%s:%s60s (orig persistent)60sSTRINGR4J match
11R4J_SUMMONER_REFRESH_COOLDOWNr4j:summoner:refresh:cooldown:%s:%s (puuid, shard)60s (orig 2m)60sSTRINGcooldown flag (nx + ex)
12MATCH_DETAILlos:%s:%s:match:%s:detail (cacheRegion, shard, gameId)60s (orig 6h)60sSTRINGMatch
13SUMMONER_DATAlos:%s:%s:summoner:%s:data60s (orig 1h)60sSTRINGsummoner data
14SUMMONER_AUTOCOMPLETElos:%s:%s:summoner:autocomplete:%s60s (orig 6h)60sSTRINGautocomplete
15SUMMONER_SEARCHlos:%s:%s:summoner:search:%s60s (orig 1h)60sSTRINGsearch
16SUMMONERlos:%s:%s:summoner:%s (cacheRegion, shard, puuid)60s (orig 4h)60sSTRINGSummoner
17SUMMONER_OVERVIEWlos:%s:%s:summoner:%s:overview60s (orig 4h)60sSTRINGSummonerView
18SUMMONER_RANKlos:%s:%s:summoner:%s:rank60s (orig 4h)60sSTRINGRank
19SUMMONER_RANKSlos:%s:%s:summoner:%s:ranks60s (orig 4h)60sSTRINGMap<GameQueueType,Rank>
20SUMMONER_MASTERIESlos:%s:%s:summoner:%s:masteries60s (orig 12h)60sSTRINGList<Mastery>
21SUMMONER_STATISTICSlos:%s:%s:summoner:%s:statistics:%s (+ filter.toSummonerKey())60s (orig 12h)60sSTRINGProfileStatistics
22SUMMONER_ACTIVITYlos:%s:%s:summoner:%s:activity:%s60s (orig 12h)60sSTRINGProfileActivity
23SUMMONER_MATCHUPSlos:%s:%s:summoner:%s:matchups:%s60s (orig 12h)60sSTRINGProfileMatchups
24SUMMONER_RANK_HISTORYlos:%s:%s:summoner:%s:rank-history:%s1d60sSTRINGRankHistory
25LEADERBOARD_PAGElos:leaderboard:page:%s:%s:%s:%s:%s:%s:%s60s (orig 1d)60sSTRINGLeaderboardPage
26LEADERBOARD_COUNTlos:leaderboard:count:%s:%s:%s:%s:%s60s60sSTRINGcount
27LEADERBOARD_COUNT_LOCKlos:leaderboard:count-lock:%s:%s:%s:%s:%s1m60sSTRINGlock
28LEADERBOARD_RANK_DISTRIBUTIONlos:leaderboard:rank-distribution:%s:%s60s (orig 12h)60sSTRINGLeaderboardDistribution
29LEADERBOARD_TOP_REGIONSlos:leaderboard:top-regions:%s:%s60s (orig 12h)60sSTRINGLeaderboardDistribution
30CONTEXTUAL_LEADERBOARD_SEGMENTlos:leaderboard:%s:%s:%s6h6h/persistZSETmember 16B SHA256(puuid) score mmr
31CONTEXTUAL_LEADERBOARD_COUNTSlos:leaderboard:counts:%s:%s0 (persist)persistHASHfield→count
32CONTEXTUAL_LEADERBOARD_SEGMENTSlos:leaderboard:segments0 (persist)persistSETsegment keys
33CONTEXTUAL_LEADERBOARD_ACCESSlos:leaderboard:access0 (persist)persistHASHaccess timestamps
34CONTEXTUAL_RECORD_SEGMENTlos:ranking:%s:%s:%s6h6h/persistZSETmember 16B score per metric
35CONTEXTUAL_RECORD_SEGMENTSlos:ranking:segments0 (persist)persistSETsegment keys
36CONTEXTUAL_RECORD_ACCESSlos:ranking:access0 (persist)persistHASHaccess
37CONTEXTUAL_LEADERBOARD_BUILDlos:leaderboard:building:%s10m10mSTRINGbuild lock
38CONTEXTUAL_RANKING_BUILDlos:ranking:building:%s10m10mSTRINGbuild lock
39CHAMPION_STATSlos:champion:%s:stats:%s60s (orig 12h)60sSTRINGChampionStatistics
40CHAMPION_PAGElos:champion:%s:page:%s60s (orig 1h)60sSTRINGchampion page
41CHAMPION_TIER_LISTlos:champion:tier-list:%s60s (orig 1d)60sSTRINGChampionTierList
42-47STATUS_*status:game-queue etc10dpersistSTRING/HASHmetrics (STATUS_GAME_QUEUE, GAMES_ANALYZED, TOTAL_SUMMONERS, TOTAL_MASTERIES, RANKS_BY_QUEUE, TRACKED_SUMMONERS)

30 main keys + 6 STATUS_* + 4 CONTEXTUAL_* persistent = 40+ patterns. The prefix beebot:lol: is always prepended (RedisKey.of :71).

Redis types

TypeKeysEncoding
STRING (JSON)all los:... except ZSET/HASH/SETJsonCodec.toJson value
ZSETCONTEXTUAL_LEADERBOARD_SEGMENT, CONTEXTUAL_RECORD_SEGMENTmember = 16B SHA256(puuid) (byte[]), score = mmr (double from TierDivisionUtils.getMmr)
HASHCONTEXTUAL_LEADERBOARD_COUNTS, ACCESS, CONTEXTUAL_RECORD_*field→long via hset/hgetAll/hincrBy
SETCONTEXTUAL_LEADERBOARD_SEGMENTS, CONTEXTUAL_RECORD_SEGMENTSsadd/smembers

Member SHA256 16B and mmr score

For leaderboard/record ZSETs (MongoDB.COMPETITIVE_ID_BYTES=16 MongoDB.java:103):

java
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(puuid.getBytes(StandardCharsets.UTF_8));
byte[] member = Arrays.copyOf(hash, 16); // 16B
double score = TierDivisionUtils.getMmr(tierDivision, lp); // mmr
// IRON_IV=0 +100/div, MASTER=10000, GM=20000, CHALL=30000 + lp

getMmr (TierDivisionUtils.java:78): UNRANKED→-1.

Pipeline operations and helpers

RedisClient exposes:

OperationMethodFile:lineNotes
GETget(key), get(key, Class), get(key, TypeRef), get(List keys, Class) via mget:96, :108, :120, :132mget pipeline for LeaderboardService
SETset(RedisKey, String/T, args), setCached(key,value,ttl), setPersistent:56, :256, :299setex with TEMPORARY_TTL_SECONDS=60
CLAIMclaim(RedisKey, value, args):64set nx ex 60 — for refresh cooldown
DELETEdelete(key), delete(List):149, :159
EXPIRE/TTL/PERSISTexpire, ttl, setPersistent, addPersistentMember:278, :266, :288
SADD/SMEMBERS/SCARD/SREMsadd, members, countMembers, removeMember, smembers (with del):183, :194, :201, :209, :235
LPUSH/LRANGE/POPrpush, lrangeAll, popList (transaction):175, :217, :224
HGETALL/HSET/HINCRBY/HDELgetHashLongs, replaceHash, incrementHashIfPresent, setHashLong, removeHashField:481, :496, :510, :520, :531
ZCARD/ZADD/ZREM/ZREVRANK/ZCOUNTsortedSetCardinality, addSortedSet, removeSortedSetMember, reverseRanks, countsAbove, existingSortedSets:457, :361, :375, :423, :440, :345pipeline
BUILD ZSETbuildSortedSet(tmpKey, key, ttl, tmpTtl, source):397batch 250, rename + expire/persist
RENAMEpublishSortedSet(tmpKey, key, ttl):385
PIPELINE EXISTSexistingSortedSets(List keys):345pipeline.exists
DBSIZE/MEMORYdbSize, usedMemory, memoryUsage:309, :321, :469via jedis.info("memory")

ZSET examples

java
// Leaderboard — reverse rank (position)
List<Long> ranks = RedisClient.reverseRanks(
    List.of("beebot:lol:los:leaderboard:EUW1:RANKED_SOLO_5X5:CHALLENGER"),
    List.of(member16B) // byte[]
); // zrevrank

// Count above score
List<Long> counts = RedisClient.countsAbove(
    List.of("beebot:lol:los:leaderboard:EUW1:RANKED_SOLO_5X5"),
    List.of(15000.0) // mmr
); // zcount (score, +inf)

// Pipeline mget for LeaderboardPage
Map<String, Summoner> map = RedisClient.get(keys, Summoner.class); // mget

// Atomic build
boolean ok = RedisClient.buildSortedSet(tmpKey, key, 60, 60, emitter -> {
    for (Entry e : entries) emitter.accept(new SortedSetEntry(member16B, e.mmr()));
});
// addSortedSet → expire tmp → rename → expire/persist

Circuit breaker — 30s

java
private static final long RETRY_AFTER_FAILURE_MS = 30_000; // :30
private static volatile long disabledUntil; // :32
private static boolean canUseRedis() { return System.currentTimeMillis() >= disabledUntil; } // :545
private static void markUnavailable() { disabledUntil = System.currentTimeMillis() + 30_000; } // :549
private static void markAvailable() { disabledUntil = 0; } // :553

Every catch (Exception) calls markUnavailable(), every success markAvailable(). If Redis is down, for 30s all get/set return null/false without touching the pool — graceful degradation to direct Mongo.

Prefix beebot:lol

java
// RedisKey.java:67
this.prefix = App.isTesting() ? "beebot_test:lol" : "beebot:lol";
public String of(Object... args) { return String.format(prefix + ":" + pattern, args); }

Example prod: beebot:lol:los:EUW1:EUW1:summoner:abc-puuid:overview (SUMMONER_OVERVIEW with cacheRegion=EUROPE from LeagueShardUtils.cacheRegion :52).

Read-through pattern

java
// MatchService.java:65 and ProfileService.java:46
Match cached = RedisClient.get(RedisKey.MATCH_DETAIL.of(cacheRegion, shard, gameId), Match.class);
if (cached != null) return ApiResult.ready(cached);
Match stored = MongoDB.findMatch(gameId);
if (stored != null) RedisClient.set(RedisKey.MATCH_DETAIL, stored, cacheRegion, shard, gameId);
return stored;

Same for SUMMONER_OVERVIEW, SUMMONER_STATISTICS (filter.toSummonerKey()), LEADERBOARD_PAGE — always get → miss → Mongo → set.