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 totrue
.- Tweet will be posted because
extendedMaintenancePostedFlags.posted24HourWarning
isfalse
. 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 tofalse
.- 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
- Allow a warning tweet to be posted, e.g. 11AM ET for the 2 hour warning
- End the Express instance
- Set clock to 10:58AM ET and restart the Express instance
- 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.999
→00:00:06.012
while I am checking for seconds to be within 6 second intervalscurrentDate.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.
Close issue, merge