From bca0682e9332f2557198255072e985ead5365d3b Mon Sep 17 00:00:00 2001 From: "James M. Greene" Date: Thu, 7 Jan 2021 13:50:24 -0600 Subject: [PATCH 1/3] Add release script to soft-purge all rendered pages from the Redis cache (#17164) * Create a release script to soft-purge all rendered pages from the Redis cache * Set NODE_ENV * Pass the Redis database number as an option rather than in the URL * Change key scanning pattern based on Heroku metadata presence * Shorten purge TTL to 30 minutes * Only fail hard on Heroku production releases * Don't return TOO early or else we forget to resume the scanStream! * Correct ioredis command casing to all lowercase * Add unexpectedly necessary exit * Tweak wording of dry run logging * Add some polish * Prevent accidental soft-purging of the current release's keys * Simplify the key check * Fix lint error --- Procfile | 2 + script/purge-redis-pages.js | 122 ++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 script/purge-redis-pages.js diff --git a/Procfile b/Procfile index 94f69e48bb..6d92caa7fb 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,3 @@ web: NODE_ENV=production node server.js + +release: NODE_ENV=production node script/purge-redis-pages.js diff --git a/script/purge-redis-pages.js b/script/purge-redis-pages.js new file mode 100644 index 0000000000..bc02728e2e --- /dev/null +++ b/script/purge-redis-pages.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +// [start-readme] +// +// Run this script to manually purge the Redis rendered page cache. +// This will typically only be run by Heroku during the deployment process, +// as triggered via our Procfile's "release" phase configuration. +// +// [end-readme] + +const program = require('commander') +const Redis = require('ioredis') + +const { REDIS_URL, HEROKU_RELEASE_VERSION, HEROKU_PRODUCTION_APP } = process.env +const isHerokuProd = HEROKU_PRODUCTION_APP === 'true' +const pageCacheDatabaseNumber = 1 +const keyScanningPattern = HEROKU_RELEASE_VERSION ? '*:rp:*' : 'rp:*' +const scanSetSize = 250 + +const startTime = Date.now() +const expirationDuration = 30 * 60 * 1000 // 30 minutes +const expirationTimestamp = startTime + expirationDuration // 30 minutes from now + +program + .description('Purge the Redis rendered page cache') + .option('-d, --dry-run', 'print keys to be purged without actually purging') + .parse(process.argv) + +const dryRun = program.dryRun + +// verify environment variables +if (!REDIS_URL) { + if (isHerokuProd) { + console.error('Error: you must specify the REDIS_URL environment variable.\n') + process.exit(1) + } else { + console.warn('Warning: you did not specify a REDIS_URL environment variable. Exiting...\n') + process.exit(0) + } +} + +console.log({ + HEROKU_RELEASE_VERSION, + HEROKU_PRODUCTION_APP +}) + +purgeRenderedPageCache() + +function purgeRenderedPageCache () { + const redisClient = new Redis(REDIS_URL, { db: pageCacheDatabaseNumber }) + let totalKeyCount = 0 + let iteration = 0 + + // Create a readable stream (object mode) for the SCAN cursor + const scanStream = redisClient.scanStream({ + match: keyScanningPattern, + count: scanSetSize + }) + + scanStream.on('end', function () { + console.log(`Done purging keys; affected total: ${totalKeyCount}`) + console.log(`Time elapsed: ${Date.now() - startTime} ms`) + + // This seems to be unexpectedly necessary + process.exit(0) + }) + + scanStream.on('error', function (error) { + console.error('An unexpected error occurred!\n' + error.stack) + console.error('\nAborting...') + process.exit(1) + }) + + scanStream.on('data', async function (keys) { + console.log(`[Iteration ${iteration++}] Received ${keys.length} keys...`) + + // NOTE: It is possible for a SCAN cursor iteration to return 0 keys when + // using a MATCH because it is applied after the elements are retrieved + if (keys.length === 0) return + + if (dryRun) { + console.log(`DRY RUN! This iteration might have set TTL for up to ${keys.length} keys:\n - ${keys.join('\n - ')}`) + return + } + + // Pause the SCAN stream while we set a TTL on these keys + scanStream.pause() + + // Find existing TTLs to ensure we aren't extending the TTL if it's already set + // PTTL mykey // only operate on -1 result values or those greater than ONE_HOUR_FROM_NOW + const pttlPipeline = redisClient.pipeline() + keys.forEach(key => pttlPipeline.pttl(key)) + const pttlResults = await pttlPipeline.exec() + + // Update pertinent keys to have TTLs set + let updatingKeyCount = 0 + const pexpireAtPipeline = redisClient.pipeline() + keys.forEach((key, i) => { + const [error, pttl] = pttlResults[i] + const needsShortenedTtl = error == null && (pttl === -1 || pttl > expirationDuration) + const isOldKey = !HEROKU_RELEASE_VERSION || !key.startsWith(`${HEROKU_RELEASE_VERSION}:`) + + if (needsShortenedTtl && isOldKey) { + pexpireAtPipeline.pexpireat(key, expirationTimestamp) + updatingKeyCount += 1 + } + }) + + // Only update TTLs if there are records worth updating + if (updatingKeyCount > 0) { + // Set all the TTLs + const pexpireAtResults = await pexpireAtPipeline.exec() + const updatedResults = pexpireAtResults.filter(([error, result]) => error == null && result === 1) + + // Count only the entries whose TTLs were successfully updated + totalKeyCount += updatedResults.length + } + + // Resume the SCAN stream + scanStream.resume() + }) +} From 4e6c273e74e1d05d265cad08dddc64285f0d2023 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 7 Jan 2021 11:59:42 -0800 Subject: [PATCH 2/3] Fix a typo in release notes (#17178) Co-authored-by: Chiedo John <2156688+chiedo@users.noreply.github.com> --- data/release-notes/2-22/0.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/release-notes/2-22/0.yml b/data/release-notes/2-22/0.yml index 9fc838cbe5..97b50038a4 100644 --- a/data/release-notes/2-22/0.yml +++ b/data/release-notes/2-22/0.yml @@ -5,7 +5,7 @@ sections: - heading: GitHub Actions Beta notes: - | - [GitHub Actions](https://github.com/features/actions) is a powerful, flexible solution for CI/CD and workflow automation. GitHub Actions on Enteprise Server includes tools to help you manage the service, including key metrics in the Management Console, audit logs and access controls to help you control the roll out. + [GitHub Actions](https://github.com/features/actions) is a powerful, flexible solution for CI/CD and workflow automation. GitHub Actions on Enterprise Server includes tools to help you manage the service, including key metrics in the Management Console, audit logs and access controls to help you control the roll out. You will need to provide your own [storage](https://docs.github.com/en/enterprise/2.22/admin/github-actions/enabling-github-actions-and-configuring-storage) and runners for GitHub Actions. AWS S3, Azure Blob Storage and MinIO are supported. Please review the [updated minimum requirements for your platform](https://docs.github.com/en/enterprise/2.22/admin/installation/setting-up-a-github-enterprise-server-instance) before you turn on GitHub Actions. To learn more, contact the GitHub Sales team or [sign up for the beta](https://resources.github.com/beta-signup/). {% comment %} https://github.com/github/releases/issues/775 {% endcomment %} From 72140bb8bbc1e0847065a161af2fc1cf458d6af2 Mon Sep 17 00:00:00 2001 From: "James M. Greene" Date: Thu, 7 Jan 2021 14:15:10 -0600 Subject: [PATCH 3/3] Remove use of commander from release script rather than adding it as a required production dependency (#17215) --- script/purge-redis-pages.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/script/purge-redis-pages.js b/script/purge-redis-pages.js index bc02728e2e..eb5357f427 100644 --- a/script/purge-redis-pages.js +++ b/script/purge-redis-pages.js @@ -8,7 +8,6 @@ // // [end-readme] -const program = require('commander') const Redis = require('ioredis') const { REDIS_URL, HEROKU_RELEASE_VERSION, HEROKU_PRODUCTION_APP } = process.env @@ -21,12 +20,8 @@ const startTime = Date.now() const expirationDuration = 30 * 60 * 1000 // 30 minutes const expirationTimestamp = startTime + expirationDuration // 30 minutes from now -program - .description('Purge the Redis rendered page cache') - .option('-d, --dry-run', 'print keys to be purged without actually purging') - .parse(process.argv) - -const dryRun = program.dryRun +// print keys to be purged without actually purging +const dryRun = ['-d', '--dry-run'].includes(process.argv[2]) // verify environment variables if (!REDIS_URL) {