start-companion-with-load-balancer.mjs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. #!/usr/bin/env node
  2. import { spawn } from 'node:child_process'
  3. import http from 'node:http'
  4. import httpProxy from 'http-proxy'
  5. import process from 'node:process'
  6. const numInstances = 3
  7. const lbPort = 3020
  8. const companionStartPort = 3021
  9. // simple load balancer that will direct requests round robin between companion instances
  10. function createLoadBalancer (baseUrls) {
  11. const proxy = httpProxy.createProxyServer({ ws: true })
  12. let i = 0
  13. function getTarget () {
  14. return baseUrls[i % baseUrls.length]
  15. }
  16. const server = http.createServer((req, res) => {
  17. const target = getTarget()
  18. // console.log('req', req.method, target, req.url)
  19. proxy.web(req, res, { target }, (err) => {
  20. console.error('Load balancer failed to proxy request', err.message)
  21. res.statusCode = 500
  22. res.end()
  23. })
  24. i++
  25. })
  26. server.on('upgrade', (req, socket, head) => {
  27. const target = getTarget()
  28. // console.log('upgrade', target, req.url)
  29. proxy.ws(req, socket, head, { target }, (err) => {
  30. console.error('Load balancer failed to proxy websocket', err.message)
  31. console.error(err)
  32. socket.destroy()
  33. })
  34. i++
  35. })
  36. server.listen(lbPort)
  37. console.log('Load balancer listening', lbPort)
  38. return server
  39. }
  40. const isWindows = process.platform === 'win32'
  41. const isOSX = process.platform === 'darwin'
  42. const startCompanion = ({ name, port }) => {
  43. const cp = spawn(process.execPath, [
  44. '-r', 'dotenv/config',
  45. // Watch mode support is limited to Windows and macOS at the time of writing.
  46. ...(isWindows || isOSX ? ['--watch-path', 'packages/@uppy/companion/src', '--watch'] : []),
  47. './packages/@uppy/companion/src/standalone/start-server.js',
  48. ], {
  49. cwd: new URL('../', import.meta.url),
  50. stdio: 'inherit',
  51. env: {
  52. // Note: these env variables will override anything set in .env
  53. ...process.env,
  54. COMPANION_PORT: port,
  55. COMPANION_SECRET: 'development', // multi instance will not work without secret set
  56. COMPANION_PREAUTH_SECRET: 'development', // multi instance will not work without secret set
  57. COMPANION_ALLOW_LOCAL_URLS: 'true',
  58. COMPANION_ENABLE_URL_ENDPOINT: 'true',
  59. COMPANION_LOGGER_PROCESS_NAME: name,
  60. },
  61. })
  62. // Adding a `then` property so the return value is awaitable:
  63. return Object.defineProperty(cp, 'then', {
  64. __proto__: null,
  65. writable: true,
  66. configurable: true,
  67. value: Promise.prototype.then.bind(new Promise((resolve, reject) => {
  68. cp.on('exit', (code) => {
  69. if (code === 0) resolve(cp)
  70. else reject(new Error(`Non-zero exit code: ${code}`))
  71. })
  72. cp.on('error', reject)
  73. })),
  74. })
  75. }
  76. const hosts = Array.from({ length: numInstances }, (_, index) => {
  77. const port = companionStartPort + index
  78. return { index, port }
  79. })
  80. console.log('Starting companion instances on ports', hosts.map(({ port }) => port))
  81. const companions = hosts.map(({ index, port }) => startCompanion({ name: `companion${index}`, port }))
  82. let loadBalancer
  83. try {
  84. loadBalancer = createLoadBalancer(hosts.map(({ port }) => `http://localhost:${port}`))
  85. await Promise.all(companions)
  86. } finally {
  87. loadBalancer?.close()
  88. companions.forEach((companion) => companion.kill())
  89. }