Skip to content

Commit 7f574c8

Browse files
authored
Branch was updated using the 'autoupdate branch' Actions workflow.
2 parents 607067c + 826961d commit 7f574c8

7 files changed

Lines changed: 651 additions & 25 deletions

File tree

content/github/site-policy/github-subprocessors-and-cookies.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ versions:
1010
free-pro-team: '*'
1111
---
1212

13-
Effective date: **January 5, 2021**
13+
Effective date: **January 6, 2021**
1414

1515
GitHub provides a great deal of transparency regarding how we use your data, how we collect your data, and with whom we share your data. To that end, we provide this page, which details [our subprocessors](#github-subprocessors), and how we use [cookies](#cookies-on-github).
1616

@@ -25,11 +25,9 @@ When we share your information with third party subprocessors, such as our vendo
2525
| Braintree (PayPal) | Subscription credit card payment processor | United States | United States |
2626
| Clearbit | Marketing data enrichment service | United States | United States |
2727
| Discourse | Community forum software provider | United States | United States |
28-
| DiscoverOrg | Marketing data enrichment service | United States | United States |
2928
| Eloqua | Marketing campaign automation | United States | United States |
3029
| Google Apps | Internal company infrastructure | United States | United States |
3130
| Google Analytics | Analytics and performance | United States | United States |
32-
| LinkedIn Navigator | Data enrichment service | United States | United States |
3331
| MailChimp | Customer ticketing mail services provider | United States | United States |
3432
| Mailgun | Transactional mail services provider | United States | United States |
3533
| Microsoft | Microsoft Services | United States | United States |

lib/redis-accessor.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const Redis = require('ioredis')
2+
const InMemoryRedis = require('ioredis-mock')
3+
4+
const { CI, NODE_ENV, REDIS_URL, REDIS_MAX_DB } = process.env
5+
6+
// Do not use real a Redis client for CI, tests, or if the REDIS_URL is not provided
7+
const useRealRedis = !CI && NODE_ENV !== 'test' && !!REDIS_URL
8+
9+
// By default, every Redis instance supports database numbers 0 - 15
10+
const redisMaxDb = REDIS_MAX_DB || 15
11+
12+
// Enable better stack traces in non-production environments
13+
const redisOptions = {
14+
showFriendlyErrorStack: NODE_ENV !== 'production'
15+
}
16+
17+
class RedisAccessor {
18+
constructor ({ databaseNumber = 0, prefix = null, allowSetFailures = false } = {}) {
19+
if (!Number.isInteger(databaseNumber) || databaseNumber < 0 || databaseNumber > redisMaxDb) {
20+
throw new TypeError(
21+
`Redis database number must be an integer between 0 and ${redisMaxDb} but was: ${JSON.stringify(databaseNumber)}`
22+
)
23+
}
24+
25+
const redisUrl = `${REDIS_URL}/${databaseNumber}`
26+
const redisClient = useRealRedis ? new Redis(redisUrl, redisOptions) : new InMemoryRedis()
27+
this._client = redisClient
28+
29+
this._prefix = prefix ? prefix.replace(/:+$/, '') + ':' : ''
30+
31+
// Allow for graceful failures if a Redis SET operation fails?
32+
this._allowSetFailures = allowSetFailures === true
33+
}
34+
35+
/** @private */
36+
prefix (key) {
37+
if (typeof key !== 'string' || !key) {
38+
throw new TypeError(`Key must be a non-empty string but was: ${JSON.stringify(key)}`)
39+
}
40+
41+
return this._prefix + key
42+
}
43+
44+
static translateSetArguments (options = {}) {
45+
const setArgs = []
46+
47+
const defaults = {
48+
newOnly: false,
49+
existingOnly: false,
50+
expireIn: null, // No expiration
51+
rollingExpiration: true
52+
}
53+
const opts = { ...defaults, ...options }
54+
55+
if (opts.newOnly === true) {
56+
if (opts.existingOnly === true) {
57+
throw new TypeError('Misconfiguration: entry cannot be both new and existing')
58+
}
59+
setArgs.push('NX')
60+
} else if (opts.existingOnly === true) {
61+
setArgs.push('XX')
62+
}
63+
64+
if (Number.isFinite(opts.expireIn)) {
65+
const ttl = Math.round(opts.expireIn)
66+
if (ttl < 1) {
67+
throw new TypeError('Misconfiguration: cannot set a TTL of less than 1 millisecond')
68+
}
69+
setArgs.push('PX')
70+
setArgs.push(ttl)
71+
}
72+
// otherwise there is no expiration
73+
74+
if (opts.rollingExpiration === false) {
75+
if (opts.newOnly === true) {
76+
throw new TypeError('Misconfiguration: cannot keep an existing TTL on a new entry')
77+
}
78+
setArgs.push('KEEPTTL')
79+
}
80+
81+
return setArgs
82+
}
83+
84+
async set (key, value, options = {}) {
85+
const fullKey = this.prefix(key)
86+
87+
if (typeof value !== 'string' || !value) {
88+
throw new TypeError(`Value must be a non-empty string but was: ${JSON.stringify(value)}`)
89+
}
90+
91+
// Handle optional arguments
92+
const setArgs = this.constructor.translateSetArguments(options)
93+
94+
try {
95+
const result = await this._client.set(fullKey, value, ...setArgs)
96+
return result === 'OK'
97+
} catch (err) {
98+
const errorText = `Failed to set value in Redis.
99+
Key: ${fullKey}
100+
Error: ${err.message}`
101+
102+
if (this._allowSetFailures === true) {
103+
// Allow for graceful failure
104+
console.error(errorText)
105+
return false
106+
}
107+
108+
throw new Error(errorText)
109+
}
110+
}
111+
112+
async get (key) {
113+
const value = await this._client.get(this.prefix(key))
114+
return value
115+
}
116+
117+
async exists (key) {
118+
const result = await this._client.exists(this.prefix(key))
119+
return result === 1
120+
}
121+
}
122+
123+
module.exports = RedisAccessor

middleware/rate-limit.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
const rateLimit = require('express-rate-limit')
22
const RedisStore = require('rate-limit-redis')
3+
const Redis = require('ioredis')
34

45
const isProduction = process.env.NODE_ENV === 'production'
5-
const REDIS_URL = process.env.REDIS_URL
6+
const { REDIS_URL } = process.env
7+
const rateLimitDatabaseNumber = 0
8+
const redisUrl = `${REDIS_URL}/${rateLimitDatabaseNumber}`
69

710
module.exports = rateLimit({
811
// 1 minute (or practically unlimited outside of production)
@@ -13,5 +16,5 @@ module.exports = rateLimit({
1316
// Or anything with a status code less than 400
1417
skipSuccessfulRequests: true,
1518
// When available, use Redis
16-
store: REDIS_URL && new RedisStore({ redisURL: REDIS_URL })
19+
store: REDIS_URL && new RedisStore({ client: new Redis(redisUrl) })
1720
})

middleware/render-page.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,32 @@ const layouts = require('../lib/layouts')
55
const getMiniTocItems = require('../lib/get-mini-toc-items')
66
const Page = require('../lib/page')
77
const statsd = require('../lib/statsd')
8+
const RedisAccessor = require('../lib/redis-accessor')
89

9-
// We've got lots of memory, let's use it
10-
// We can eventually throw this into redis
11-
const pageCache = {}
10+
const { HEROKU_RELEASE_VERSION } = process.env
11+
const pageCacheDatabaseNumber = 1
12+
const pageCacheExpiration = 24 * 60 * 60 * 1000 // 24 hours
13+
14+
const pageCache = new RedisAccessor({
15+
databaseNumber: pageCacheDatabaseNumber,
16+
prefix: (HEROKU_RELEASE_VERSION ? HEROKU_RELEASE_VERSION + ':' : '') + 'rp',
17+
// Allow for graceful failures if a Redis SET operation fails
18+
allowSetFailures: true
19+
})
1220

1321
module.exports = async function renderPage (req, res, next) {
1422
const page = req.context.page
1523
const originalUrl = req.originalUrl
1624

1725
// Serve from the cache if possible (skip during tests)
18-
if (!process.env.CI && process.env.NODE_ENV !== 'test') {
19-
if (req.method === 'GET' && pageCache[originalUrl]) {
26+
const isCacheable = !process.env.CI && process.env.NODE_ENV !== 'test' && req.method === 'GET'
27+
28+
if (isCacheable) {
29+
const cachedHtml = await pageCache.get(originalUrl)
30+
if (cachedHtml) {
2031
console.log(`Serving from cached version of ${originalUrl}`)
2132
statsd.increment('page.sent_from_cache')
22-
return res.send(pageCache[originalUrl])
33+
return res.send(cachedHtml)
2334
}
2435
}
2536

@@ -88,13 +99,11 @@ module.exports = async function renderPage (req, res, next) {
8899

89100
const output = await liquid.parseAndRender(layout, context)
90101

91-
// Save output to cache for the next time around
92-
if (!process.env.CI) {
93-
if (req.method === 'GET') {
94-
pageCache[originalUrl] = output
95-
}
96-
}
102+
// First, send the response so the user isn't waiting
103+
res.send(output)
97104

98-
// send response
99-
return res.send(output)
105+
// Finally, save output to cache for the next time around
106+
if (isCacheable) {
107+
await pageCache.set(originalUrl, output, { expireIn: pageCacheExpiration })
108+
}
100109
}

0 commit comments

Comments
 (0)