From c26ab2977fb658232d4790b02678fff5157e0a9b Mon Sep 17 00:00:00 2001
From: Dragory <2606411+Dragory@users.noreply.github.com>
Date: Wed, 14 Apr 2021 00:19:39 +0300
Subject: [PATCH] Attempt another fix for counter decay deadlocks

---
 backend/src/data/GuildCounters.ts | 87 ++++++++++++++++---------------
 1 file changed, 46 insertions(+), 41 deletions(-)

diff --git a/backend/src/data/GuildCounters.ts b/backend/src/data/GuildCounters.ts
index 4cbe1681..d3c2389a 100644
--- a/backend/src/data/GuildCounters.ts
+++ b/backend/src/data/GuildCounters.ts
@@ -12,12 +12,15 @@ import { CounterTriggerState } from "./entities/CounterTriggerState";
 import moment from "moment-timezone";
 import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
 import { connection } from "./db";
+import { Queue } from "../Queue";
 
 const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS;
 const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS;
 
 const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
 
+const decayQueue = new Queue();
+
 async function deleteCountersMarkedToBeDeleted(): Promise<void> {
   await getRepository(Counter)
     .createQueryBuilder()
@@ -158,53 +161,55 @@ export class GuildCounters extends BaseGuildRepository {
     );
   }
 
-  async decay(id: number, decayPeriodMs: number, decayAmount: number) {
-    const counter = (await this.counters.findOne({
-      where: {
-        id,
-      },
-    }))!;
+  decay(id: number, decayPeriodMs: number, decayAmount: number) {
+    return decayQueue.add(async () => {
+      const counter = (await this.counters.findOne({
+        where: {
+          id,
+        },
+      }))!;
 
-    const diffFromLastDecayMs = moment.utc().diff(moment.utc(counter.last_decay_at!), "ms");
-    if (diffFromLastDecayMs < decayPeriodMs) {
-      return;
-    }
+      const diffFromLastDecayMs = moment.utc().diff(moment.utc(counter.last_decay_at!), "ms");
+      if (diffFromLastDecayMs < decayPeriodMs) {
+        return;
+      }
 
-    const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
-    if (decayAmountToApply === 0) {
-      return;
-    }
+      const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
+      if (decayAmountToApply === 0) {
+        return;
+      }
 
-    // Calculate new last_decay_at based on the rounded decay amount we applied. This makes it so that over time, the decayed amount will stay accurate, even if we round some here.
-    const newLastDecayDate = moment
-      .utc(counter.last_decay_at)
-      .add((decayAmountToApply / decayAmount) * decayPeriodMs, "ms")
-      .format(DBDateFormat);
+      // Calculate new last_decay_at based on the rounded decay amount we applied. This makes it so that over time, the decayed amount will stay accurate, even if we round some here.
+      const newLastDecayDate = moment
+        .utc(counter.last_decay_at)
+        .add((decayAmountToApply / decayAmount) * decayPeriodMs, "ms")
+        .format(DBDateFormat);
 
-    const rawUpdate =
-      decayAmountToApply >= 0
-        ? `GREATEST(value - ${decayAmountToApply}, 0)`
-        : `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`;
+      const rawUpdate =
+        decayAmountToApply >= 0
+          ? `GREATEST(value - ${decayAmountToApply}, 0)`
+          : `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`;
 
-    // Using an UPDATE with ORDER BY in an attempt to avoid deadlocks from simultaneous decays
-    // Also see https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html
-    await this.counterValues
-      .createQueryBuilder("CounterValue")
-      .where("counter_id = :id", { id })
-      .orderBy("id")
-      .update({
-        value: () => rawUpdate,
-      })
-      .execute();
+      // Using an UPDATE with ORDER BY in an attempt to avoid deadlocks from simultaneous decays
+      // Also see https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html
+      await this.counterValues
+        .createQueryBuilder("CounterValue")
+        .where("counter_id = :id", { id })
+        .orderBy("id")
+        .update({
+          value: () => rawUpdate,
+        })
+        .execute();
 
-    await this.counters.update(
-      {
-        id,
-      },
-      {
-        last_decay_at: newLastDecayDate,
-      },
-    );
+      await this.counters.update(
+        {
+          id,
+        },
+        {
+          last_decay_at: newLastDecayDate,
+        },
+      );
+    });
   }
 
   async markUnusedTriggersToBeDeleted(triggerIdsToKeep: number[]) {