mirror of
https://github.com/pockethost/pockethost.git
synced 2025-05-11 18:29:54 +00:00
enh: stresser
This commit is contained in:
parent
522586e651
commit
ed974e5b4e
124
packages/daemon/src/stresser/commands/stress.ts
Normal file
124
packages/daemon/src/stresser/commands/stress.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { clientService } from '$services'
|
||||
import { InstanceId, serialAsyncExecutionGuard } from '@pockethost/common'
|
||||
import { random, range, shuffle, values } from '@s-libs/micro-dash'
|
||||
import { Command } from 'commander'
|
||||
import fetch from 'node-fetch'
|
||||
import { ContextBase, GlobalOptions } from '../types'
|
||||
|
||||
export type StressOptions = GlobalOptions & {
|
||||
instanceCount: number
|
||||
requestsPerInstance: number
|
||||
minDelay: number
|
||||
maxDelay: number
|
||||
}
|
||||
export const createStress = (context: { program: Command } & ContextBase) => {
|
||||
const { program } = context
|
||||
const logger = context.logger.create(`createStress`)
|
||||
const { dbg, error } = logger
|
||||
|
||||
const seedCmd = program.command('stress')
|
||||
seedCmd
|
||||
.description('Seed system with new instances')
|
||||
.description('Stress the system')
|
||||
.option(
|
||||
'-ic, --instance-count <number>',
|
||||
`Number of simultaneous instances to hit`,
|
||||
parseInt,
|
||||
100
|
||||
)
|
||||
.option(
|
||||
'-rc, --requests-per-instance <number>',
|
||||
`Number of simultaneous requests per instance`,
|
||||
parseInt,
|
||||
50
|
||||
)
|
||||
.option(
|
||||
'-mind, --min-delay <number>',
|
||||
`Minimum number of milliseconds to delay before sending another request`,
|
||||
parseInt,
|
||||
50
|
||||
)
|
||||
.option(
|
||||
'-maxd, --max-delay <number>',
|
||||
`Maximum number of milliseconds to delay before sending another request`,
|
||||
parseInt,
|
||||
500
|
||||
)
|
||||
.action(async () => {
|
||||
const options = seedCmd.optsWithGlobals<StressOptions>()
|
||||
|
||||
const { client } = await clientService({
|
||||
url: options.mothershipUrl,
|
||||
logger,
|
||||
})
|
||||
|
||||
const users = await client.client.collection('users').getFullList()
|
||||
dbg(users)
|
||||
|
||||
const { instanceCount, requestsPerInstance, minDelay, maxDelay } = options
|
||||
|
||||
const excluded: { [_: string]: boolean } = {}
|
||||
const resetInstance = serialAsyncExecutionGuard(
|
||||
async (instanceId: InstanceId) => {
|
||||
if (excluded[instanceId]) return
|
||||
await client.updateInstance(instanceId, { maintenance: false })
|
||||
},
|
||||
(id) => `reset:${id}`
|
||||
)
|
||||
|
||||
const instances = await client.getInstances()
|
||||
dbg(`Instances ${instances.length}`)
|
||||
|
||||
/**
|
||||
* Stress test
|
||||
*/
|
||||
const stress = async () => {
|
||||
try {
|
||||
const instance = shuffle(instances)
|
||||
.filter((v) => !excluded[v.id])
|
||||
.pop()
|
||||
dbg(
|
||||
`There are ${instances.length} instances and ${
|
||||
values(excluded).length
|
||||
} excluded`
|
||||
)
|
||||
if (!instance) throw new Error(`No instance to grab`)
|
||||
|
||||
{
|
||||
const { subdomain, id } = instance
|
||||
await resetInstance(id)
|
||||
const thisLogger = logger.create(subdomain)
|
||||
thisLogger.breadcrumb(id)
|
||||
|
||||
await Promise.all(
|
||||
range(requestsPerInstance).map(async (i) => {
|
||||
const requestLogger = thisLogger.create(`${i}`)
|
||||
const { dbg } = requestLogger
|
||||
const url = `https://${subdomain}.pockethost.test/_`
|
||||
dbg(`Fetching ${url}`)
|
||||
const res = await fetch(url)
|
||||
if (res.status !== 200) {
|
||||
const body = res.body?.read().toString()
|
||||
dbg(`${url} response error ${res.status} ${body}`)
|
||||
if (body?.match(/maintenance/i)) {
|
||||
dbg(`Maintenance mode detected. Excluding`)
|
||||
excluded[id] = true
|
||||
}
|
||||
if (res.status === 403 && !!body?.match(/Timeout/)) {
|
||||
return // Timeout
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
error(`failed with: ${e}`, e)
|
||||
} finally {
|
||||
setTimeout(stress, random(minDelay, maxDelay))
|
||||
}
|
||||
}
|
||||
range(Math.min(instances.length, instanceCount)).forEach(() => {
|
||||
stress()
|
||||
})
|
||||
})
|
||||
}
|
@ -4,6 +4,7 @@ import { logger as loggerService } from '@pockethost/common'
|
||||
import { Command } from 'commander'
|
||||
import { createCleanup } from './commands/cleanup'
|
||||
import { createSeed } from './commands/seed'
|
||||
import { createStress } from './commands/stress'
|
||||
const program = new Command()
|
||||
|
||||
loggerService({ debug: DEBUG, trace: TRACE, errorTrace: !DEBUG })
|
||||
@ -26,25 +27,7 @@ program
|
||||
|
||||
createCleanup({ program, logger })
|
||||
|
||||
const stressCmd = program.command('stress')
|
||||
stressCmd
|
||||
.description('Stress the system')
|
||||
.option(
|
||||
'-ic, --instance-count <number>',
|
||||
`Number of simultaneous instances to hit`,
|
||||
parseInt,
|
||||
100
|
||||
)
|
||||
.option(
|
||||
'-rc, --request-count <number>',
|
||||
`Number of simultaneous requests per instance`,
|
||||
parseInt,
|
||||
50
|
||||
)
|
||||
.action(async () => {
|
||||
const options = stressCmd.optsWithGlobals()
|
||||
dbg(options)
|
||||
})
|
||||
createStress({ program, logger })
|
||||
|
||||
createSeed({ program, logger })
|
||||
program.parse()
|
||||
|
@ -1,116 +0,0 @@
|
||||
import { DEBUG } from '$constants'
|
||||
import { clientService } from '$services'
|
||||
import {
|
||||
InstanceId,
|
||||
InstanceStatus,
|
||||
serialAsyncExecutionGuard,
|
||||
} from '@pockethost/common'
|
||||
import { random, range, shuffle, values } from '@s-libs/micro-dash'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
const nanoid = customAlphabet(`abcdefghijklmnop`)
|
||||
|
||||
const THREAD_COUNT = 1
|
||||
const REQUESTS_PER_THREAD = 1
|
||||
const CLEANUP = true
|
||||
|
||||
// npm install eventsource --save
|
||||
//@ts-ignore
|
||||
global.EventSource = require('eventsource')
|
||||
;(async () => {
|
||||
info(`Starting`)
|
||||
info(DEBUG)
|
||||
|
||||
const [nodePath, scriptPath, url] = process.argv
|
||||
if (!url) {
|
||||
throw new Error(`URL is required (http://127.0.0.1:8090)`)
|
||||
}
|
||||
|
||||
const _unsafe_createInstance = async () => {
|
||||
await client.createInstance({
|
||||
subdomain: `stress-${nanoid()}`,
|
||||
uid: shuffle(users).pop()!.id,
|
||||
status: InstanceStatus.Idle,
|
||||
version: `~0.${random(1, 16)}.0`,
|
||||
secondsThisMonth: 0,
|
||||
secrets: {},
|
||||
maintenance: false,
|
||||
})
|
||||
}
|
||||
const createInstance = serialAsyncExecutionGuard(_unsafe_createInstance)
|
||||
|
||||
const { client } = await clientService({ url, logger })
|
||||
const users = await client.client.collection('users').getFullList()
|
||||
dbg(users)
|
||||
|
||||
/**
|
||||
* Create instances
|
||||
*/
|
||||
await Promise.all(range(10).map(() => createInstance()))
|
||||
const instances = await client.getInstances()
|
||||
if (CLEANUP) {
|
||||
}
|
||||
dbg(`Instances ${instances.length}`)
|
||||
|
||||
const excluded: { [_: string]: boolean } = {}
|
||||
const resetInstance = serialAsyncExecutionGuard(
|
||||
async (instanceId: InstanceId) => {
|
||||
if (excluded[instanceId]) return
|
||||
await client.updateInstance(instanceId, { maintenance: false })
|
||||
},
|
||||
(id) => `reset:${id}`
|
||||
)
|
||||
|
||||
/**
|
||||
* Stress test
|
||||
*/
|
||||
const stress = async () => {
|
||||
try {
|
||||
const instance = shuffle(instances)
|
||||
.filter((v) => !excluded[v.id])
|
||||
.pop()
|
||||
dbg(
|
||||
`There are ${instances.length} instances and ${
|
||||
values(excluded).length
|
||||
} excluded`
|
||||
)
|
||||
if (!instance) throw new Error(`No instance to grab`)
|
||||
|
||||
{
|
||||
const { subdomain, id } = instance
|
||||
await resetInstance(id)
|
||||
const thisLogger = logger.create(subdomain)
|
||||
thisLogger.breadcrumb(id)
|
||||
|
||||
await Promise.all(
|
||||
range(REQUESTS_PER_THREAD).map(async (i) => {
|
||||
const requestLogger = thisLogger.create(`${i}`)
|
||||
const { dbg } = requestLogger
|
||||
const url = `https://${subdomain}.pockethost.test/_`
|
||||
dbg(`Fetching ${url}`)
|
||||
const res = await fetch(url)
|
||||
if (res.status !== 200) {
|
||||
const body = res.body?.read().toString()
|
||||
dbg(`${url} response error ${res.status} ${body}`)
|
||||
if (body?.match(/maintenance/i)) {
|
||||
dbg(`Maintenance mode detected. Excluding`)
|
||||
excluded[id] = true
|
||||
}
|
||||
if (res.status === 403 && !!body?.match(/Timeout/)) {
|
||||
return // Timeout
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
error(`${url} failed with: ${e}`, JSON.stringify(e))
|
||||
} finally {
|
||||
setTimeout(stress, random(50, 500))
|
||||
}
|
||||
}
|
||||
range(THREAD_COUNT).forEach(() => {
|
||||
stress()
|
||||
})
|
||||
})()
|
Loading…
x
Reference in New Issue
Block a user