feat: make redis optional, fall back to less consistent local cache

This commit is contained in:
shiftinv 2022-09-07 15:28:25 +02:00
parent 6027c8271d
commit 75ba80e63c
4 changed files with 41 additions and 12 deletions

View File

@ -2,3 +2,4 @@ export * as http from "https://deno.land/std@0.154.0/http/mod.ts";
export * as log from "https://deno.land/std@0.154.0/log/mod.ts";
export * as hex from "https://deno.land/std@0.154.0/encoding/hex.ts";
export * as redis from "https://deno.land/x/redis@v0.27.0/mod.ts";
export { default as TTLCache } from "https://deno.land/x/ttl@1.0.1/mod.ts";

View File

@ -17,7 +17,7 @@ export default {
maxWebhookRetries: parseInt(get("MAX_RETRIES", "3")),
maxWebhookRetryMs: parseInt(get("MAX_RETRY_MS", "30000")),
mainRedirect: get("MAIN_REDIRECT", null),
redisUrl: get("REDIS_URL"),
redisUrl: get("REDIS_URL", null),
// set by deno deploy
deployId: get("DENO_DEPLOYMENT_ID", "<unknown>"),

View File

@ -1,4 +1,4 @@
import { reviewCommentManager } from "./manager.ts";
import { commentManager } from "./manager.ts";
import { UrlConfig } from "./types.d.ts";
import { requestLog } from "./util.ts";
@ -36,21 +36,25 @@ export default async function filter(
if (event === "pull_request_review") {
// ignore edit/dismiss actions
if (json.action !== "submitted") return `no-op PR review action '${json.action}'`;
// if comment (not approval or changes requested), ignore empty review body
else if (json.review?.state === "commented" && !json.review?.body) return "empty PR review";
if (json.review?.state === "commented" && !json.review?.body) return "empty PR review";
}
// ignore some PR comment events
if (event === "pull_request_review_comment") {
// ignore edit/delete actions
if (json.action !== "created") return `no-op PR comment action '${json.action}'`;
// check if more than x comments on a PR review in a short timespan
const reviewId: number = json.comment?.pull_request_review_id;
if (config.commentBurstLimit && reviewId) {
const cacheKey = `${reviewId}-${login}`;
reqLog.debug(`filter: checking cache key ${cacheKey}`);
const curr = await reviewCommentManager.getAndIncrement(cacheKey);
const curr = await commentManager.getAndIncrement(cacheKey);
reqLog.debug(`filter: current value: ${curr}`);
if (curr && curr >= config.commentBurstLimit) {
return `exceeded comment burst limit (${config.commentBurstLimit}) for review ${reviewId}`;
}

View File

@ -1,23 +1,45 @@
import { redis } from "../deps.ts";
import { redis, TTLCache } from "../deps.ts";
import config from "./config.ts";
class ReviewCommentManager {
private redis: redis.Redis;
const KEY_EXPIRY = 3; // seconds
interface CommentManager {
getAndIncrement(key: string): Promise<number>;
}
class LocalCommentManager implements CommentManager {
private cache: TTLCache<number>;
constructor() {
this.cache = new TTLCache(KEY_EXPIRY * 1000);
}
getAndIncrement(key: string): Promise<number> {
const value = this.cache.get(key) ?? 0;
this.cache.set(key, value + 1);
return Promise.resolve(value);
}
}
class RedisCommentManager implements CommentManager {
private redis: redis.Redis;
constructor(redisUrl: string) {
this.redis = redis.createLazyClient({
...redis.parseURL(config.redisUrl),
...redis.parseURL(redisUrl),
maxRetryCount: 3,
});
}
async getAndIncrement(key: string): Promise<number> {
key = `reviewcomment:${key}`;
// seems like pipelines don't quite work with the lazy client, so force a connection
if (!this.redis.isConnected) {
// seems like pipelines don't quite work with the lazy client
await this.redis.ping();
}
const pl = this.redis.pipeline();
pl.incr(`reviewcomment:${key}`);
pl.expire(`reviewcomment:${key}`, 3);
pl.incr(key);
pl.expire(key, KEY_EXPIRY);
const results = await pl.flush();
const newValue = results[0] as number;
@ -25,4 +47,6 @@ class ReviewCommentManager {
}
}
export const reviewCommentManager = new ReviewCommentManager();
export const commentManager = config.redisUrl
? new RedisCommentManager(config.redisUrl)
: new LocalCommentManager();