custom Fastly surrogate key for api search (#32536)

This commit is contained in:
Peter Bengtsson 2022-11-21 13:34:06 +01:00 коммит произвёл GitHub
Родитель 4e159d7e74
Коммит f189f9d900
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 112 добавлений и 17 удалений

Просмотреть файл

@ -126,6 +126,13 @@ jobs:
curl --retry-connrefused --retry 5 ${{ env.ELASTICSEARCH_URL }}/_cat/indices?v
curl --retry-connrefused --retry 5 ${{ env.ELASTICSEARCH_URL }}/_cat/indices?v
- name: Purge Fastly edge cache
env:
FASTLY_TOKEN: ${{ secrets.FASTLY_TOKEN }}
FASTLY_SERVICE_ID: ${{ secrets.FASTLY_SERVICE_ID }}
FASTLY_SURROGATE_KEY: api-search:${{ matrix-language }}
run: .github/actions-scripts/purge-fastly-edge-cache.js
- name: Send Slack notification if workflow fails
uses: someimportantcompany/github-actions-slack-message@f8d28715e7b8a4717047d23f48c39827cacad340
if: failure()

Просмотреть файл

@ -5,14 +5,22 @@ import FailBot from '../../lib/failbot.js'
import languages from '../../lib/languages.js'
import { allVersions } from '../../lib/all-versions.js'
import statsd from '../../lib/statsd.js'
import { defaultCacheControl } from '../cache-control.js'
import { cacheControlFactory } from '../cache-control.js'
import catchMiddlewareError from '../catch-middleware-error.js'
import { setFastlySurrogateKey } from '../set-fastly-surrogate-key.js'
import {
getSearchResults,
POSSIBLE_HIGHLIGHT_FIELDS,
DEFAULT_HIGHLIGHT_FIELDS,
} from './es-search.js'
// This means we tell the browser to cache the XHR request for 1h
const browserCacheControl = cacheControlFactory(60 * 60)
// This tells the CDN to cache the response for 4 hours
const cdnCacheControl = cacheControlFactory(60 * 60 * 4, {
key: 'surrogate-control',
})
// Used by the legacy search
const versions = new Set(Object.values(searchVersions))
const languagesSet = new Set(Object.keys(languages))
@ -149,7 +157,9 @@ router.get(
}
})
if (process.env.NODE_ENV !== 'development') {
defaultCacheControl(res)
browserCacheControl(res)
cdnCacheControl(res)
setFastlySurrogateKey(res, `api-search:${language}`, true)
}
res.setHeader('x-search-legacy', 'yes')
@ -248,7 +258,8 @@ router.get(
'/v1',
validationMiddleware,
catchMiddlewareError(async function search(req, res) {
const { indexName, query, autocomplete, page, size, debug, sort, highlights } = req.search
const { indexName, language, query, autocomplete, page, size, debug, sort, highlights } =
req.search
// The getSearchResults() function is a mix of preparing the search,
// sending & receiving it, and post-processing the response from the
@ -276,12 +287,9 @@ router.get(
statsd.timing('api.search.query', meta.took.query_msec, tags)
if (process.env.NODE_ENV !== 'development') {
// The assumption, at the moment is that searches are never distinguished
// differently depending on a cookie or a request header.
// So the only distinguishing key is the request URL.
// Because of that, it's safe to allow the reverse proxy (a.k.a the CDN)
// cache and hold on to this.
defaultCacheControl(res)
browserCacheControl(res)
cdnCacheControl(res)
setFastlySurrogateKey(res, `api-search:${language}`, true)
}
// The v1 version of the output matches perfectly what comes out

Просмотреть файл

@ -14,11 +14,13 @@ export const SURROGATE_ENUMS = {
MANUAL: 'manual-purge',
}
export function setFastlySurrogateKey(res, enumKey) {
if (!Object.values(SURROGATE_ENUMS).includes(enumKey)) {
throw new Error(
`Unrecognizes surrogate enumKey. ${enumKey} is not one of ${Object.values(SURROGATE_ENUMS)}`
)
export function setFastlySurrogateKey(res, enumKey, isCustomKey = false) {
if (process.env.NODE_ENV !== 'production') {
if (!isCustomKey && !Object.values(SURROGATE_ENUMS).includes(enumKey)) {
throw new Error(
`Unrecognizes surrogate enumKey. ${enumKey} is not one of ${Object.values(SURROGATE_ENUMS)}`
)
}
}
res.set(KEY, enumKey)
}

Просмотреть файл

@ -182,7 +182,7 @@
"build": "next build",
"debug": "cross-env NODE_ENV=development ENABLED_LANGUAGES='en,ja,ru' nodemon --inspect server.js",
"dev": "cross-env npm start",
"index-test-fixtures": "node script/search/index-elasticsearch.js -l en -V ghae -V dotcom --index-prefix tests -- tests/content/fixtures/search-indexes",
"index-test-fixtures": "node script/search/index-elasticsearch.js -l en -l ja -V ghae -V dotcom --index-prefix tests -- tests/content/fixtures/search-indexes",
"lint": "eslint '**/*.{js,mjs,ts,tsx}'",
"lint-translation": "cross-env NODE_OPTIONS=--experimental-vm-modules TEST_TRANSLATION=true jest tests/linting/lint-files.js",
"prepare": "husky install",

Просмотреть файл

@ -15,7 +15,6 @@ import { jest, test, expect } from '@jest/globals'
import { describeIfElasticsearchURL } from '../helpers/conditional-runs.js'
import { get } from '../helpers/e2etest.js'
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
if (!process.env.ELASTICSEARCH_URL) {
console.warn(
@ -69,7 +68,47 @@ describeIfElasticsearchURL('search v1 middleware', () => {
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-control']).toContain('public')
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.DEFAULT)
expect(res.headers['surrogate-key']).toBe('api-search:en')
})
test('basic search in Japanese', async () => {
const sp = new URLSearchParams()
// To see why this will work,
// see tests/content/fixtures/search-indexes/github-docs-dotcom-en-records.json
// which clearly has a record with the title "Foo"
sp.set('query', 'foo')
sp.set('language', 'ja')
const res = await get('/api/search/v1?' + sp)
expect(res.statusCode).toBe(200)
const results = JSON.parse(res.text)
expect(results.meta).toBeTruthy()
expect(results.meta.found.value).toBeGreaterThanOrEqual(1)
expect(results.meta.found.relation).toBeTruthy()
expect(results.meta.page).toBe(1)
expect(results.meta.size).toBeGreaterThanOrEqual(1)
expect(results.meta.took.query_msec).toBeGreaterThanOrEqual(0)
expect(results.meta.took.total_msec).toBeGreaterThanOrEqual(0)
// Might be empty but at least an array
expect(results.hits).toBeTruthy()
// The word 'foo' appears in more than 1 document in the fixtures.
expect(results.hits.length).toBeGreaterThanOrEqual(1)
// ...but only one has the word "foo" in its title so we can
// be certain it comes first.
const hit = results.hits[0]
// This specifically checks what we expect of version v1
expect(hit.url).toBe('/ja/foo')
expect(hit.title).toBe('フー')
expect(hit.breadcrumbs).toBe('fooing')
// Check that it can be cached at the CDN
expect(res.headers['set-cookie']).toBeUndefined()
expect(res.headers['cache-control']).toContain('public')
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-control']).toContain('public')
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
expect(res.headers['surrogate-key']).toBe('api-search:ja')
})
test('debug search', async () => {

Просмотреть файл

@ -0,0 +1,29 @@
{
"/ja/foo": {
"objectID": "/ja/foo",
"breadcrumbs": "fooing",
"title": "フー",
"headings": "",
"content": "これは、ばかげた foo ワードのフィクスチャです。",
"topics": ["Test", "Fixture"],
"popularity": 0.5
},
"/ja/bar": {
"objectID": "/ja/bar",
"breadcrumbs": "baring",
"title": "バー",
"headings": "",
"content": "bar も持っていない場合、foo を持つことはできません。",
"topics": ["Test", "Fixture", "Get started"],
"popularity": 0.6
},
"/ja/breadcrumbless": {
"objectID": "/ja/bar",
"breadcrumbs": "",
"title": "パンくずなし",
"headings": "",
"content": "すべてのページにブレッドクラムがあるわけではありません。人生の事実。",
"topics": [],
"popularity": 0.1
}
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"/ja/github-ae@latest/foo": {
"objectID": "/ja/github-ae@latest/foo",
"breadcrumbs": "foo",
"title": "フー",
"headings": "",
"content": "これは、ばかげた foo ワードのフィクスチャです。 GHAE専用。",
"topics": ["Test", "Fixture"]
}
}