perf: reduce the initial timeout for proxy connections, make configurable (#31283)

* use cached preflight response on subsequent cloud requests that require a preflight

* introduce env var to skip initial proxy-api request

* rename env to CYPRESS_INTERNAL prefix

* change noproxy timeout default to 5 seconds

* fix

* rm internal

* changelog

* Update packages/server/test/unit/cloud/api/api_spec.js

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>

* Update packages/server/lib/cloud/api/index.ts

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* fix ts

---------

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
Co-authored-by: Bill Glesias <bglesias@gmail.com>
This commit is contained in:
Cacie Prins
2025-04-17 10:00:46 -04:00
committed by GitHub
parent 2d8c2247e1
commit a50012b892
4 changed files with 125 additions and 27 deletions
+4
View File
@@ -3,6 +3,10 @@
_Released 4/22/2025 (PENDING)_
**Performance:**
- Reduced the initial timeout for the preflight API request to determine proxy conditions from sixty seconds to five, and made this timeout duration configurable with the `CYPRESS_INITIAL_PREFLIGHT_TIMEOUT` environment variable. Addresses [#28423](https://github.com/cypress-io/cypress/issues/28423). Addressed in [#31283](https://github.com/cypress-io/cypress/pull/31283).
**Bugfixes:**
- The [`cy.press()`](http://on.cypress.io/api/press) command no longer errors when used in specs subsequent to the first spec in run mode. Fixes [#31466](https://github.com/cypress-io/cypress/issues/31466).
+49 -17
View File
@@ -6,14 +6,19 @@ const request = require('@cypress/request-promise')
const humanInterval = require('human-interval')
const RequestErrors = require('@cypress/request-promise/errors')
const { agent } = require('@packages/network')
const pkg = require('@packages/root')
const machineId = require('../machine_id')
const errors = require('../../errors')
const { apiUrl, apiRoutes, makeRoutes } = require('../routes')
import Bluebird from 'bluebird'
import type { AfterSpecDurations } from '@packages/types'
import { agent } from '@packages/network'
import type { CombinedAgent } from '@packages/network/lib/agent'
import { apiUrl, apiRoutes, makeRoutes } from '../routes'
import { getText } from '../../util/status_code'
import * as enc from '../encryption'
import getEnvInformationForProjectRoot from '../environment'
@@ -22,7 +27,7 @@ import type { OptionsWithUrl } from 'request-promise'
import { fs } from '../../util/fs'
import ProtocolManager from '../protocol'
import type { ProjectBase } from '../../project-base'
import type { AfterSpecDurations } from '@packages/types'
import { PUBLIC_KEY_VERSION } from '../constants'
// axios implementation disabled until proxy issues can be diagnosed/fixed
@@ -235,6 +240,16 @@ const isRetriableError = (err) => {
(err.statusCode == null)
}
function noProxyPreflightTimeout (): number {
try {
const timeoutFromEnv = Number(process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT)
return isNaN(timeoutFromEnv) ? 5000 : timeoutFromEnv
} catch (e: unknown) {
return 5000
}
}
export type CreateRunOptions = {
projectRoot: string
ci: {
@@ -307,8 +322,21 @@ type UpdateInstanceArtifactsOptions = {
instanceId: string
timeout?: number
}
interface DefaultPreflightResult {
encrypt: true
}
let preflightResult = {
interface PreflightWarning {
message: string
}
interface CachedPreflightResult {
encrypt: boolean
apiUrl: string
warnings?: PreflightWarning[]
}
let preflightResult: DefaultPreflightResult | CachedPreflightResult = {
encrypt: true,
}
@@ -598,14 +626,13 @@ export default {
sendPreflight (preflightInfo) {
return retryWithBackoff(async (attemptIndex) => {
const { timeout, projectRoot } = preflightInfo
preflightInfo = _.omit(preflightInfo, 'timeout', 'projectRoot')
const { projectRoot, timeout, ...preflightRequestBody } = preflightInfo
const preflightBaseProxy = apiUrl.replace('api', 'api-proxy')
const envInformation = await getEnvInformationForProjectRoot(projectRoot, process.pid.toString())
const makeReq = ({ baseUrl, agent }) => {
const makeReq = (baseUrl: string, agent: CombinedAgent | null, timeout: number) => {
return rp.post({
url: `${baseUrl}preflight`,
body: {
@@ -613,13 +640,13 @@ export default {
envUrl: envInformation.envUrl,
dependencies: envInformation.dependencies,
errors: envInformation.errors,
...preflightInfo,
...preflightRequestBody,
},
headers: {
'x-route-version': '1',
'x-cypress-request-attempt': attemptIndex,
},
timeout: timeout ?? SIXTY_SECONDS,
timeout,
json: true,
encrypt: 'always',
agent,
@@ -631,14 +658,19 @@ export default {
}
const postReqs = async () => {
return makeReq({ baseUrl: preflightBaseProxy, agent: null })
.catch((err) => {
if (err.statusCode === 412) {
throw err
}
const initialPreflightTimeout = noProxyPreflightTimeout()
return makeReq({ baseUrl: apiUrl, agent })
})
if (initialPreflightTimeout >= 0) {
try {
return await makeReq(preflightBaseProxy, null, initialPreflightTimeout)
} catch (err) {
if (err.statusCode === 412) {
throw err
}
}
}
return makeReq(apiUrl, agent, timeout)
}
const result = await postReqs()
+5 -8
View File
@@ -2,7 +2,8 @@ import _ from 'lodash'
import UrlParse from 'url-parse'
const app_config = require('../../config/app.json')
const apiUrl = app_config[process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'development'].api_url
export const apiUrl = app_config[process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'development'].api_url
const CLOUD_ENDPOINTS = {
api: '',
@@ -40,7 +41,7 @@ const parseArgs = function (url, args: any[] = []) {
return url
}
const makeRoutes = (baseUrl: string, routes: typeof CLOUD_ENDPOINTS) => {
const _makeRoutes = (baseUrl: string, routes: typeof CLOUD_ENDPOINTS) => {
return _.reduce(routes, (memo, value, key) => {
memo[key] = function (...args: any[]) {
let url = new UrlParse(baseUrl, true)
@@ -60,10 +61,6 @@ const makeRoutes = (baseUrl: string, routes: typeof CLOUD_ENDPOINTS) => {
}, {} as Record<keyof typeof CLOUD_ENDPOINTS, (...args: any[]) => string>)
}
const apiRoutes = makeRoutes(apiUrl, CLOUD_ENDPOINTS)
export const apiRoutes = _makeRoutes(apiUrl, CLOUD_ENDPOINTS)
module.exports = {
apiUrl,
apiRoutes,
makeRoutes: (baseUrl) => makeRoutes(baseUrl, CLOUD_ENDPOINTS),
}
export const makeRoutes = (baseUrl) => _makeRoutes(baseUrl, CLOUD_ENDPOINTS)
@@ -245,6 +245,8 @@ describe('lib/cloud/api', () => {
require('../../../../lib/cloud/encryption')
}, module)
}
prodApi.resetPreflightResult()
})
it('POST /preflight to proxy. returns encryption', () => {
@@ -323,12 +325,75 @@ describe('lib/cloud/api', () => {
})
})
it('sets timeout to 60 seconds', () => {
it('sets timeout to 5 seconds when no CYPRESS_INITIAL_PREFLIGHT_TIMEOUT env is set', () => {
sinon.stub(api.rp, 'post').resolves({})
return api.sendPreflight({})
.then(() => {
expect(api.rp.post).to.be.calledWithMatch({ timeout: 60000 })
expect(api.rp.post).to.be.calledWithMatch({ timeout: 5000 })
})
})
describe('when CYPRESS_INITIAL_PREFLIGHT_TIMEOUT env is set to a negative number', () => {
const configuredTimeout = -1
let prevEnv
beforeEach(() => {
prevEnv = process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = configuredTimeout
})
afterEach(() => {
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = prevEnv
})
it('skips the no-agent preflight request', () => {
preflightNock(API_PROD_PROXY_BASEURL)
.replyWithError('should not be called')
preflightNock(API_PROD_BASEURL)
.reply(200, decryptReqBodyAndRespond({
reqBody: {
envUrl: 'https://some.server.com',
dependencies: {},
errors: [],
apiUrl: 'https://api.cypress.io/',
projectId: 'abc123',
},
resBody: {
encrypt: true,
apiUrl: `${API_PROD_BASEURL}/`,
},
}))
return prodApi.sendPreflight({ projectId: 'abc123' })
.then((ret) => {
expect(ret).to.deep.eq({ encrypt: true, apiUrl: `${API_PROD_BASEURL}/` })
})
})
})
describe('when CYPRESS_INITIAL_PREFLIGHT_TIMEOUT env is set to a positive number', () => {
const configuredTimeout = 10000
let prevEnv
beforeEach(() => {
prevEnv = process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = configuredTimeout
})
afterEach(() => {
process.env.CYPRESS_INITIAL_PREFLIGHT_TIMEOUT = prevEnv
api.rp.post.restore()
})
it('makes the initial request with the number set in the env', () => {
sinon.stub(api.rp, 'post').resolves({})
return api.sendPreflight({})
.then(() => {
expect(api.rp.post).to.be.calledWithMatch({ timeout: configuredTimeout })
})
})
})