(batch: list[tuple])
| 259 | |
| 260 | |
| 261 | async def _write_rollup(batch: list[tuple]) -> None: |
| 262 | counts: dict[tuple[str, str, str, str], int] = defaultdict(int) |
| 263 | for ts, user_id, service, _host in batch: |
| 264 | counts[(user_id, ts.strftime("%Y-%m"), service, ts.strftime("%d"))] += 1 |
| 265 | |
| 266 | # Group the batch by hash first, so chunking splits between hashes rather |
| 267 | # than through the middle of one and every EXPIRE lands with its HINCRBYs. |
| 268 | by_key: dict[str, list] = defaultdict(list) |
| 269 | for (user_id, month, service, day), n in counts.items(): |
| 270 | by_key[_rollup_key(user_id, month)] += [_rollup_field(service, day), n] |
| 271 | |
| 272 | global _rollup_script |
| 273 | redis = await get_redis() |
| 274 | if _rollup_script is None: |
| 275 | _rollup_script = redis.register_script(_ROLLUP_LUA) |
| 276 | |
| 277 | items = list(by_key.items()) |
| 278 | for start in range(0, len(items), ROLLUP_CHUNK): |
| 279 | chunk = items[start:start + ROLLUP_CHUNK] |
| 280 | # Keys are addressed by index from ARGV so each hash is named once, |
| 281 | # however many of its fields the batch touches. |
| 282 | keys = [k for k, _ in chunk] |
| 283 | argv: list = [ROLLUP_TTL_DAYS * 86400] |
| 284 | for i, (_key, fields) in enumerate(chunk, start=1): |
| 285 | for j in range(0, len(fields), 2): |
| 286 | argv += [i, fields[j], fields[j + 1]] |
| 287 | # Each chunk is independent. A failure part-way through leaves earlier |
| 288 | # chunks applied, which is the same partial-batch outcome the caller |
| 289 | # already tolerates - these counters are explicitly approximate, and |
| 290 | # HINCRBY is not idempotent so retrying would double-count. |
| 291 | await _rollup_script(keys=keys, args=argv) |
| 292 | |
| 293 | |
| 294 | async def _flush(batch: list[tuple]) -> None: |
no test coverage detected