enh: dynamic port allocation

This commit is contained in:
Ben Allfree 2024-11-12 00:59:15 -08:00
parent 4d96492353
commit 7f63c48008
6 changed files with 42 additions and 65 deletions

View File

@ -41,7 +41,6 @@
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-sslify": "^1.2.0", "express-sslify": "^1.2.0",
"ftp-srv": "github:pockethost/ftp-srv#0fc708bae0d5d7a55ce948767f082d6fcfb2af59", "ftp-srv": "github:pockethost/ftp-srv#0fc708bae0d5d7a55ce948767f082d6fcfb2af59",
"get-port": "^6.1.2",
"glob": "^10.3.10", "glob": "^10.3.10",
"gobot": "1.0.0-alpha.41", "gobot": "1.0.0-alpha.41",
"gobot-pocketbase": "0.22.8-alpha.22", "gobot-pocketbase": "0.22.8-alpha.22",

View File

@ -19,7 +19,6 @@ import {
mkContainerHomePath, mkContainerHomePath,
mkDocUrl, mkDocUrl,
mkInstanceUrl, mkInstanceUrl,
mkInternalUrl,
mkSingleton, mkSingleton,
now, now,
stringify, stringify,
@ -29,7 +28,6 @@ import {
InstanceLogger, InstanceLogger,
MothershipAdminClientService, MothershipAdminClientService,
PocketbaseService, PocketbaseService,
PortService,
SpawnConfig, SpawnConfig,
proxyService, proxyService,
} from '../../services' } from '../../services'
@ -121,24 +119,10 @@ export const instanceService = mkSingleton(
updateInstanceStatus(id, InstanceStatus.Idle) updateInstanceStatus(id, InstanceStatus.Idle)
}) })
/** Obtain empty port */
dbg(`Obtaining port`)
const [newPortPromise, releasePort] = PortService().alloc()
const newPort = await newPortPromise
shutdownManager.push(() => {
dbg(`shut down: releasing port`)
releasePort()
})
systemInstanceLogger.breadcrumb({ port: newPort })
dbg(`Found port`)
const internalUrl = mkInternalUrl(newPort)
dbg(`internalUrl`, internalUrl)
/** Create spawn config */ /** Create spawn config */
const spawnArgs: SpawnConfig = { const spawnArgs: SpawnConfig = {
subdomain: instance.subdomain, subdomain: instance.subdomain,
instanceId: instance.id, instanceId: instance.id,
port: newPort,
dev: instance.dev, dev: instance.dev,
extraBinds: flatten([ extraBinds: flatten([
globSync(join(INSTANCE_APP_MIGRATIONS_DIR(), '*.js')).map( globSync(join(INSTANCE_APP_MIGRATIONS_DIR(), '*.js')).map(
@ -178,7 +162,7 @@ export const instanceService = mkSingleton(
/** Spawn the child process */ /** Spawn the child process */
const childProcess = await pbService.spawn(spawnArgs) const childProcess = await pbService.spawn(spawnArgs)
const { exitCode, stopped, started } = childProcess const { exitCode, stopped, started, url: internalUrl } = childProcess
shutdownManager.push(() => { shutdownManager.push(() => {
dbg(`killing ${id}`) dbg(`killing ${id}`)

View File

@ -18,14 +18,12 @@ import {
} from '../../../core' } from '../../../core'
import { GobotService } from '../GobotService' import { GobotService } from '../GobotService'
import { InstanceLogger } from '../InstanceLoggerService' import { InstanceLogger } from '../InstanceLoggerService'
import { PortService } from '../PortService'
export type Env = { [_: string]: string } export type Env = { [_: string]: string }
export type SpawnConfig = { export type SpawnConfig = {
subdomain: string subdomain: string
instanceId: string instanceId: string
version?: string version?: string
port?: number
extraBinds?: string[] extraBinds?: string[]
env?: Env env?: Env
stdout?: MemoryStream stdout?: MemoryStream
@ -65,16 +63,9 @@ export const createPocketbaseService = async (
const cm = createCleanupManager() const cm = createCleanupManager()
const logger = LoggerService().create('spawn') const logger = LoggerService().create('spawn')
const { dbg, warn, error } = logger const { dbg, warn, error } = logger
const port =
cfg.port ||
(await (async () => {
const [defaultPort, freeDefaultPort] = await PortService().alloc()
cm.add(freeDefaultPort)
return defaultPort
})())
const _cfg: Required<SpawnConfig> = { const _cfg: Required<SpawnConfig> = {
version: maxVersion, version: maxVersion,
port,
extraBinds: [], extraBinds: [],
env: {}, env: {},
stderr: new MemoryStream(), stderr: new MemoryStream(),
@ -119,6 +110,7 @@ export const createPocketbaseService = async (
const container = await new Promise<{ const container = await new Promise<{
on: EventEmitter['on'] on: EventEmitter['on']
kill: () => Promise<void> kill: () => Promise<void>
portBinding: number
}>((resolve, reject) => { }>((resolve, reject) => {
const docker = new Docker() const docker = new Docker()
iLogger.info(`Starting instance`) iLogger.info(`Starting instance`)
@ -153,7 +145,7 @@ export const createPocketbaseService = async (
HostConfig: { HostConfig: {
AutoRemove: true, AutoRemove: true,
PortBindings: { PortBindings: {
'8090/tcp': [{ HostPort: `${port}` }], '8090/tcp': [{ HostPort: `0` }],
}, },
Binds, Binds,
Ulimits: [ Ulimits: [
@ -225,17 +217,44 @@ export const createPocketbaseService = async (
} }
}, },
) )
.on('start', (container: Container) => { .on('start', async (container: Container) => {
dbg(`Got started container`, container) dbg(`Got started container`, container)
started = true started = true
resolve({
on: emitter.on.bind(emitter), try {
kill: () => // Get container info to retrieve the assigned port
container.stop({ signal: `SIGINT` }).catch((e) => { const containerInfo = await container.inspect()
error(e) const ports = containerInfo.NetworkSettings?.Ports?.['8090/tcp']
return container.stop({ signal: `SIGKILL` }).catch(error)
}), if (!ports || !ports[0] || !ports[0].HostPort) {
}) throw new Error('Could not get port binding from container')
}
const portBinding = parseInt(ports[0].HostPort, 10)
if (isNaN(portBinding)) {
throw new Error(`Invalid port binding: ${ports[0].HostPort}`)
}
resolve({
on: emitter.on.bind(emitter),
kill: () =>
container.stop({ signal: `SIGINT` }).catch((e) => {
error(e)
return container.stop({ signal: `SIGKILL` }).catch(error)
}),
portBinding,
})
} catch (e) {
error(`Failed to get port binding: ${e}`)
reject(e)
try {
await container.stop()
} catch (stopError) {
error(
`Failed to stop container after port binding error: ${stopError}`,
)
}
}
}) })
}).catch((e) => { }).catch((e) => {
error(`Error starting container: ${e}`) error(`Error starting container: ${e}`)
@ -255,7 +274,8 @@ export const createPocketbaseService = async (
dbg(`Instance exited with ${code}`) dbg(`Instance exited with ${code}`)
cm.shutdown().catch(error) cm.shutdown().catch(error)
}) })
const url = mkInternalUrl(port)
const url = mkInternalUrl(container.portBinding)
logger.breadcrumb({ url }) logger.breadcrumb({ url })
dbg(`Making exit hook for ${url}`) dbg(`Making exit hook for ${url}`)
const unsub = asyncExitHook(async () => { const unsub = asyncExitHook(async () => {

View File

@ -1,16 +0,0 @@
import getPort from 'get-port'
import { INITIAL_PORT_POOL_SIZE } from '../../core'
import { Allocator, ResourceAllocator, ioc } from '../common'
export type Config = { maxPorts: number }
export const PortService = () => {
try {
return ioc<Allocator<number>>('portAllocator')
} catch (e) {
return ioc(
'portAllocator',
ResourceAllocator(INITIAL_PORT_POOL_SIZE(), getPort),
)
}
}

View File

@ -2,6 +2,5 @@ export * from './InstanceLoggerService'
export * from './InstanceService' export * from './InstanceService'
export * from './MothershipAdminClientService' export * from './MothershipAdminClientService'
export * from './PocketBaseService' export * from './PocketBaseService'
export * from './PortService'
export * from './ProxyService' export * from './ProxyService'
export * from './RealtimeLog' export * from './RealtimeLog'

9
pnpm-lock.yaml generated
View File

@ -370,9 +370,6 @@ importers:
ftp-srv: ftp-srv:
specifier: github:pockethost/ftp-srv#0fc708bae0d5d7a55ce948767f082d6fcfb2af59 specifier: github:pockethost/ftp-srv#0fc708bae0d5d7a55ce948767f082d6fcfb2af59
version: https://codeload.github.com/pockethost/ftp-srv/tar.gz/0fc708bae0d5d7a55ce948767f082d6fcfb2af59 version: https://codeload.github.com/pockethost/ftp-srv/tar.gz/0fc708bae0d5d7a55ce948767f082d6fcfb2af59
get-port:
specifier: ^6.1.2
version: 6.1.2
glob: glob:
specifier: ^10.3.10 specifier: ^10.3.10
version: 10.4.5 version: 10.4.5
@ -2968,10 +2965,6 @@ packages:
resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==} resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==}
engines: {node: '>=4'} engines: {node: '>=4'}
get-port@6.1.2:
resolution: {integrity: sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
get-stream@2.3.1: get-stream@2.3.1:
resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -7778,8 +7771,6 @@ snapshots:
get-port@3.2.0: {} get-port@3.2.0: {}
get-port@6.1.2: {}
get-stream@2.3.1: get-stream@2.3.1:
dependencies: dependencies:
object-assign: 4.1.1 object-assign: 4.1.1