Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use load balancer for companion in e2e tests #4228

Merged
merged 16 commits into from
Feb 2, 2023
Merged
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,8 @@ module.exports = {
extends: ['plugin:cypress/recommended'],
},
{
files: ['e2e/**/*.ts', 'e2e/**/*.js', 'e2e/**/*.jsx'],
rules: { 'import/no-extraneous-dependencies': 'off', 'no-unused-expressions': 'off' },
files: ['e2e/**/*.ts', 'e2e/**/*.js', 'e2e/**/*.jsx', 'e2e/**/*.mjs'],
rules: { 'import/no-extraneous-dependencies': 'off', 'no-unused-expressions': 'off', 'no-console': 'off' },
},
],
}
19 changes: 14 additions & 5 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: lts/*

- name: Start Redis
uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 7

- name: Install dependencies
run: corepack yarn install --immutable
env:
Expand All @@ -69,18 +75,21 @@ jobs:
- name: Run end-to-end browser tests
run: corepack yarn run e2e:ci
env:
COMPANION_DATADIR: ./output
COMPANION_DOMAIN: localhost:3020
COMPANION_PROTOCOL: http
COMPANION_REDIS_URL: redis://localhost:6379
COMPANION_UNSPLASH_KEY: ${{secrets.COMPANION_UNSPLASH_KEY}}
COMPANION_UNSPLASH_SECRET: ${{secrets.COMPANION_UNSPLASH_SECRET}}
COMPANION_AWS_KEY: ${{secrets.COMPANION_AWS_KEY}}
COMPANION_AWS_SECRET: ${{secrets.COMPANION_AWS_SECRET}}
COMPANION_AWS_BUCKET: ${{secrets.COMPANION_AWS_BUCKET}}
COMPANION_AWS_REGION: ${{secrets.COMPANION_AWS_REGION}}
VITE_COMPANION_URL: http://localhost:3020
VITE_TRANSLOADIT_KEY: ${{secrets.TRANSLOADIT_KEY}}
VITE_TRANSLOADIT_SECRET: ${{secrets.TRANSLOADIT_SECRET}}
VITE_TRANSLOADIT_TEMPLATE: ${{secrets.TRANSLOADIT_TEMPLATE}}
VITE_TRANSLOADIT_SERVICE_URL: ${{secrets.TRANSLOADIT_SERVICE_URL}}
COMPANION_AWS_KEY: ${{secrets.COMPANION_AWS_KEY}}
COMPANION_AWS_SECRET: ${{secrets.COMPANION_AWS_SECRET}}
COMPANION_AWS_BUCKET: ${{secrets.COMPANION_AWS_BUCKET}}
COMPANION_AWS_REGION: ${{secrets.COMPANION_AWS_REGION}}
COMPANION_AWS_DISABLE_ACL: 'true'
# https://docs.cypress.io/guides/references/advanced-installation#Binary-cache
CYPRESS_CACHE_FOLDER: ${{ steps.cypress-cache-dir-path.outputs.dir }}
- name: Upload videos in case of failure
Expand Down
5 changes: 1 addition & 4 deletions e2e/cypress.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'cypress'
// eslint-disable-next-line import/no-extraneous-dependencies
import installLogsPrinter from 'cypress-terminal-report/src/installLogsPrinter.js'

export default defineConfig({
Expand All @@ -10,8 +8,7 @@ export default defineConfig({
baseUrl: 'http://localhost:1234',
specPattern: 'cypress/integration/*.spec.ts',

// eslint-disable-next-line no-unused-vars
setupNodeEvents (on, config) {
setupNodeEvents (on) {
// implement node event listeners here
installLogsPrinter(on)
},
Expand Down
1 change: 0 additions & 1 deletion e2e/generate-test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env node
/* eslint-disable no-console, import/no-extraneous-dependencies */
import prompts from 'prompts'
import fs from 'node:fs/promises'

Expand Down
1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"cypress": "^10.0.0",
"cypress-terminal-report": "^4.1.2",
"deep-freeze": "^0.0.1",
"execa": "^6.1.0",
"parcel": "^2.0.1",
"prompts": "^2.4.2",
"react": "^18.1.0",
Expand Down
78 changes: 78 additions & 0 deletions e2e/start-companion-with-load-balancer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node

import { execa } from 'execa'
import http from 'node:http'
import httpProxy from 'http-proxy'

const numInstances = 3
const lbPort = 3020
const companionStartPort = 3021

// simple load balancer that will direct requests round robin between companion instances
function createLoadBalancer (baseUrls) {
const proxy = httpProxy.createProxyServer({ ws: true })

let i = 0

function getTarget () {
return baseUrls[i % baseUrls.length]
}

const server = http.createServer((req, res) => {
const target = getTarget()
// console.log('req', req.method, target, req.url)
proxy.web(req, res, { target }, (err) => {
console.error('Load balancer failed to proxy request', err.message)
res.statusCode = 500
res.end()
})
i++
})

server.on('upgrade', (req, socket, head) => {
const target = getTarget()
// console.log('upgrade', target, req.url)
proxy.ws(req, socket, head, { target }, (err) => {
console.error('Load balancer failed to proxy websocket', err.message)
console.error(err)
socket.destroy()
})
i++
})

server.listen(lbPort)
console.log('Load balancer listening', lbPort)
return server
}

const startCompanion = ({ name, port }) => execa('nodemon', [
'--watch', 'packages/@uppy/companion/src', '--exec', 'node', '-r', 'dotenv/config', './packages/@uppy/companion/src/standalone/start-server.js',
], {
stdio: 'inherit',
mifi marked this conversation as resolved.
Show resolved Hide resolved
env: {
// Note: these env variables will override anything set in .env
COMPANION_PORT: port,
COMPANION_SECRET: 'development', // multi instance will not work without secret set
COMPANION_PREAUTH_SECRET: 'development', // multi instance will not work without secret set
COMPANION_ALLOW_LOCAL_URLS: 'true',
COMPANION_LOGGER_PROCESS_NAME: name,
},
})

const hosts = Array.from({ length: numInstances }, (_, index) => {
const port = companionStartPort + index
return { index, port }
})

console.log('Starting companion instances on ports', hosts.map(({ port }) => port))

const companions = hosts.map(({ index, port }) => startCompanion({ name: `companion${index}`, port }))

let loadBalancer
try {
loadBalancer = createLoadBalancer(hosts.map(({ port }) => `http://localhost:${port}`))
await Promise.all(companions)
} finally {
loadBalancer?.close()
companions.forEach((companion) => companion.kill())
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,11 @@
"release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive",
"size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
"start:companion": "bash bin/companion.sh",
"start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
"start": "npm-run-all --parallel watch start:companion web:start",
"e2e": "yarn build && yarn e2e:skip-build",
"e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion e2e:cypress",
"e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion' '1234|3020' e2e:headless",
"e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress",
"e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion:with-loadbalancer' '1234|3020' e2e:headless",
"e2e:client": "yarn workspace e2e client:start",
"e2e:cypress": "yarn workspace e2e cypress:open",
"e2e:headless": "yarn workspace e2e cypress:headless",
Expand Down
8 changes: 8 additions & 0 deletions packages/@uppy/companion/src/companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const { getCredentialsOverrideMiddleware } = require('./server/provider/credenti
// @ts-ignore
const { version } = require('../package.json')

function setLoggerProcessName ({ loggerProcessName }) {
if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
}

// intercepts grantJS' default response error when something goes
// wrong during oauth process.
const interceptGrantErrorResponse = interceptor((req, res) => {
Expand Down Expand Up @@ -51,13 +55,17 @@ const interceptGrantErrorResponse = interceptor((req, res) => {
module.exports.errors = { ProviderApiError, ProviderAuthError }
module.exports.socket = require('./server/socket')

module.exports.setLoggerProcessName = setLoggerProcessName

/**
* Entry point into initializing the Companion app.
*
* @param {object} optionsArg
* @returns {{ app: import('express').Express, emitter: any }}}
*/
module.exports.app = (optionsArg = {}) => {
setLoggerProcessName(optionsArg)

validateConfig(optionsArg)

const options = merge({}, defaultOptions, optionsArg)
Expand Down
34 changes: 19 additions & 15 deletions packages/@uppy/companion/src/server/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,25 @@ function maskMessage (msg) {
return out
}

let processName = 'companion'

exports.setProcessName = (newProcessName) => {
processName = newProcessName
}

/**
* message log
*
* @param {string | Error} arg the message or error to log
* @param {string} tag a unique tag to easily search for this message
* @param {string} level error | info | debug
* @param {string} [id] a unique id to easily trace logs tied to a request
* @param {Function} [color] function to display the log in appropriate color
* @param {object} params
* @param {string | Error} params.arg the message or error to log
* @param {string} params.tag a unique tag to easily search for this message
* @param {string} params.level error | info | debug
* @param {string} [params.traceId] a unique id to easily trace logs tied to a request
* @param {Function} [params.color] function to display the log in appropriate color
*/
const log = (arg, tag = '', level, id = '', color = (message) => message) => {
const log = ({ arg, tag = '', level, traceId = '', color = (message) => message }) => {
const time = new Date().toISOString()
const whitespace = tag && id ? ' ' : ''
const whitespace = tag && traceId ? ' ' : ''

function msgToString () {
// We don't need to log stack trace on special errors that we ourselves have produced
Expand All @@ -59,7 +66,7 @@ const log = (arg, tag = '', level, id = '', color = (message) => message) => {
const msgString = msgToString()
const masked = maskMessage(msgString)
// eslint-disable-next-line no-console
console.log(color(`companion: ${time} [${level}] ${id}${whitespace}${tag}`), color(masked))
console.log(color(`${processName}: ${time} [${level}] ${traceId}${whitespace}${tag}`), color(masked))
}

/**
Expand All @@ -70,7 +77,7 @@ const log = (arg, tag = '', level, id = '', color = (message) => message) => {
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.info = (msg, tag, traceId) => {
log(msg, tag, 'info', traceId)
log({ arg: msg, tag, level: 'info', traceId })
}

/**
Expand All @@ -81,8 +88,7 @@ exports.info = (msg, tag, traceId) => {
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.warn = (msg, tag, traceId) => {
// @ts-ignore
log(msg, tag, 'warn', traceId, chalk.bold.yellow)
log({ arg: msg, tag, level: 'warn', traceId, color: chalk.bold.yellow })
}

/**
Expand All @@ -93,8 +99,7 @@ exports.warn = (msg, tag, traceId) => {
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.error = (msg, tag, traceId) => {
// @ts-ignore
log(msg, tag, 'error', traceId, chalk.bold.red)
log({ arg: msg, tag, level: 'error', traceId, color: chalk.bold.red })
}

/**
Expand All @@ -106,7 +111,6 @@ exports.error = (msg, tag, traceId) => {
*/
exports.debug = (msg, tag, traceId) => {
if (process.env.NODE_ENV !== 'production') {
// @ts-ignore
log(msg, tag, 'debug', traceId, chalk.bold.blue)
log({ arg: msg, tag, level: 'debug', traceId, color: chalk.bold.blue })
}
}
Loading