enh: stresser

This commit is contained in:
Ben Allfree 2023-06-22 23:11:17 -07:00
parent 522586e651
commit ed974e5b4e
3 changed files with 126 additions and 135 deletions

View 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()
})
})
}

View File

@ -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()

View File

@ -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()
})
})()