theericzhang/eamuse-maintenance-bot

if app crashes, extendedMaintenancePostedFlags all get reset to false

theericzhang opened this issue · 7 comments

Expected Behavior

If the clock lands 1 days before Extended Maintenance, specifically at 11:00 am ET / 12:00 am jst:

  • is1DaybeforeExtendedMaintenance will set to true.
  • Tweet will be posted because extendedMaintenancePostedFlags.posted24HourWarning is false.
  • extendedMaintenancePostedFlags.posted24HourWarning will be set to true.

If the app crashes, the tweet should not post again and should wait for the next checkpoint, extendedMaintenancePostedFlags.posted2HourWarning

Actual Behavior

If the app crashes & reboots, assuming the 1 day warning has been posted already:

  • is1DaybeforeExtendedMaintenance will be re-initialized to false.
  • Tweet will attempt to post again
  • Twitter returns a "duplicate status" rejected attempt, closing connection to the API. This causes extended downtime on the bot.
  • App crashes, repeats cycle

Steps to reproduce bug

  1. Allow a warning tweet to be posted, e.g. 11AM ET for the 2 hour warning
  2. End the Express instance
  3. Set clock to 10:58AM ET and restart the Express instance
  4. Take note of Twitter's "duplicate status" rejected attempt

example crash logs from a 1 day warning double post

2022-10-17T02:06:31.246937+00:00 app[worker.1]:     'x-response-time': '102',
2022-10-17T02:06:31.246937+00:00 app[worker.1]:     'x-connection-hash': '2a0792015ff665fb2086d0ae79c31bfb53bbd67f7dac7ec7c89181fa28ec373e',
2022-10-17T02:06:31.246937+00:00 app[worker.1]:     connection: 'close'
2022-10-17T02:06:31.246938+00:00 app[worker.1]:   },
2022-10-17T02:06:31.246938+00:00 app[worker.1]:   rateLimit: undefined,
2022-10-17T02:06:31.246938+00:00 app[worker.1]:   data: { errors: [ { code: 187, message: 'Status is a duplicate.' } ] },
2022-10-17T02:06:31.246938+00:00 app[worker.1]:   errors: [ { code: 187, message: 'Status is a duplicate.' } ]
2022-10-17T02:06:31.246939+00:00 app[worker.1]: }
2022-10-17T02:06:31.403329+00:00 heroku[worker.1]: Process exited with status 1
2022-10-17T02:06:31.503170+00:00 heroku[worker.1]: State changed from up to crashed

Specs

Version: Express 4.18.2
Platform: Heroku

Suggestions

Write and load the extendedMaintenancePostedFlags to/from an external file so that state is preserved
AND OR
Guard the postTweet function with an additional condition; one that checks that it is no more than a minute past the time. For example,

const is2HoursBeforeExtendedMaintenance = isTodayExtendedMaintenance && extendedMaintenanceDay.getUTCHours() === 0 && currentDate.getMinutes() < 1;
  • cannot set flags in local storage. heroku instance is non-persistent and dynos are set to wipe every 24 hours. db may be too cumbersome
  • additional guard clause with seconds is hard to time due to milliseconds it takes to clear call stack. e.g. 23:59:59.99900:00:06.012 while I am checking for seconds to be within 6 second intervals currentDate.getSeconds() < 6
  • consider fetching last tweet and directly comparing its timestamp?

High suspicion that 6 seconds may not be enough time for the POST request promise to resolve.

e.g.

1) postTweet() gets called.
2) POST request #1 added to call stack, promise is pending.
3) 6 seconds elapse, POST promise #1 has not resolved yet, postTweet() gets called again.
4) POST request #2 added to call stack, promise is pending.
5) POST promise #1 resolves, Twitter account tweets.
6) POST promise #2 is rejected because Twitter detects same content trying to be tweeted. Connection closes, app crashes
  • extend interval time to 15+ seconds to allow for the POST request promise to resolve?
  • look into how to pause setInterval() while a promise is still pending

Suggestion to prevent an additional POST request from being added to the call stack -

let isCurrentlyPosting = false;

async function postTweet(tweetBody) {
    // set isCurrentlyPosting flag to true
    isCurrentlyPosting = true;
    await clientMain.v1.tweet(tweetBody);
    // set isCurrentlyPosting flag to false
    isCurrentlyPosting = false;
    console.log('Posted! - ', tweetBody);
}

async function extendedMaintenanceObserver() {
    !isCurrentlyPosting && postTweet();
}

todo: test this fake promise -

let isCurrentlyPosting = false;

async function postTweet(tweetBody) {
    isCurrentlyPosting = true;
    const testResponsePromise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, 6000)
    })
    testResponsePromise
        .then(() => {
            console.log('Posted! - ', tweetBody);
            isCurrentlyPosting = false;
        })
        .catch((error) => console.error(error));
}

todo: test this fake promise -

let isCurrentlyPosting = false;

async function postTweet(tweetBody) {
    isCurrentlyPosting = true;
    const testResponsePromise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, 6000)
    })
    testResponsePromise
        .then(() => {
            console.log('Posted! - ', tweetBody);
            isCurrentlyPosting = false;
        })
        .catch((error) => console.error(error));
}

using isCurrentlyPosting to guard postTweet() from being called twice works.
await is expected to behave in the same way, since it wraps a promise.

v1.tweet() is guaranteed to return a Promise, therefore await will suspend the thread. Proceed with isCurrentlyPosting guarding

Screen Shot 2022-11-07 at 4 50 30 PM

Close issue, merge