0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-25 11:17:50 +01:00
posthog/plugin-server/tests/schedule.test.ts
Yakko Majuri 149604f530
fix(plugin-server): start main thread consumer only after plugins are loaded on threads (#10385)
* fix(plugin-server): start main thread consumer only after plugins are loaded on threads

* updates
2022-06-22 11:37:30 +01:00

246 lines
8.0 KiB
TypeScript

import Piscina from '@posthog/piscina'
import { PluginEvent } from '@posthog/plugin-scaffold/src/types'
import { Redis } from 'ioredis'
import * as nodeSchedule from 'node-schedule'
import {
loadPluginSchedule,
LOCKED_RESOURCE,
runScheduleDebounced,
startPluginSchedules,
waitForTasksToFinish,
} from '../src/main/services/schedule'
import { Hub, LogLevel, PluginScheduleControl } from '../src/types'
import { createHub } from '../src/utils/db/hub'
import { delay, UUIDT } from '../src/utils/utils'
import { createPromise } from './helpers/promises'
import { resetTestDatabase } from './helpers/sql'
import { setupPiscina } from './helpers/worker'
jest.mock('../src/utils/db/sql')
jest.mock('../src/utils/status')
jest.setTimeout(60000) // 60 sec timeout
function createEvent(index = 0): PluginEvent {
return {
uuid: new UUIDT().toString(),
distinct_id: 'my_id',
ip: '127.0.0.1',
site_url: 'http://localhost',
team_id: 2,
now: new Date().toISOString(),
event: 'default event',
properties: { key: 'value', index },
}
}
function numberOfScheduledJobs() {
return Object.keys(nodeSchedule.scheduledJobs).length
}
describe('schedule', () => {
test('runScheduleDebounced', async () => {
const workerThreads = 1
const testCode = `
const counterKey = 'test_counter_2'
async function setupPlugin (meta) {
await meta.cache.set(counterKey, 0)
}
async function processEvent (event, meta) {
event.properties['counter'] = await meta.cache.get(counterKey)
return event
}
async function runEveryMinute (meta) {
// stall for a second
await new Promise(resolve => __jestSetTimeout(resolve, 500))
await meta.cache.incr(counterKey)
}
`
await resetTestDatabase(testCode)
const piscina = setupPiscina(workerThreads, 10)
const ingestEvent = async (event: PluginEvent) => {
const result = await piscina.run({ task: 'runEventPipeline', args: { event } })
const resultEvent = result.args[0]
return resultEvent
}
const [hub, closeHub] = await createHub({ LOG_LEVEL: LogLevel.Log })
hub.pluginSchedule = await loadPluginSchedule(piscina)
expect(hub.pluginSchedule).toEqual({ runEveryDay: [], runEveryHour: [], runEveryMinute: [39] })
const event1 = await ingestEvent(createEvent())
expect(event1.properties['counter']).toBe(0)
runScheduleDebounced(hub, piscina, 'runEveryMinute')
runScheduleDebounced(hub, piscina, 'runEveryMinute')
runScheduleDebounced(hub, piscina, 'runEveryMinute')
await delay(100)
const event2 = await ingestEvent(createEvent())
expect(event2.properties['counter']).toBe(0)
await delay(500)
const event3 = await ingestEvent(createEvent())
expect(event3.properties['counter']).toBe(1)
await waitForTasksToFinish(hub)
await delay(1000)
await piscina.destroy()
await closeHub()
})
test('runScheduleDebounced exception', async () => {
const workerThreads = 2
const testCode = `
async function runEveryMinute (meta) {
throw new Error('lol')
}
`
await resetTestDatabase(testCode)
const piscina = setupPiscina(workerThreads, 10)
const [hub, closeHub] = await createHub({ LOG_LEVEL: LogLevel.Log })
hub.pluginSchedule = await loadPluginSchedule(piscina)
runScheduleDebounced(hub, piscina, 'runEveryMinute')
await waitForTasksToFinish(hub)
// nothing bad should have happened. the error is in SQL via setError, but that ran in another worker (can't mock)
// and we're not testing it E2E so we can't check the DB either...
try {
await delay(1000)
await piscina.destroy()
await closeHub()
} catch {}
})
describe('startPluginSchedules', () => {
let hub: Hub
let piscina: Piscina, closeHub: () => Promise<void>
let redis: Redis
let workerThreads: number
beforeEach(async () => {
workerThreads = 2
const testCode = `
async function runEveryMinute (meta) {
throw new Error('lol')
}
`
await resetTestDatabase(testCode)
piscina = setupPiscina(workerThreads, 10)
;[hub, closeHub] = await createHub({ LOG_LEVEL: LogLevel.Log, SCHEDULE_LOCK_TTL: 3 })
redis = await hub.redisPool.acquire()
await redis.del(LOCKED_RESOURCE)
})
afterEach(async () => {
await redis.del(LOCKED_RESOURCE)
await hub.redisPool.release(redis)
await delay(1000)
await piscina.destroy()
await closeHub()
})
test('redlock', async () => {
const promises = [createPromise(), createPromise(), createPromise()]
let i = 0
let lock1 = false
let lock2 = false
let lock3 = false
const schedule1 = await startPluginSchedules(hub, piscina, () => {
lock1 = true
promises[i++].resolve()
})
await promises[0].promise
expect(lock1).toBe(true)
expect(lock2).toBe(false)
expect(lock3).toBe(false)
const schedule2 = await startPluginSchedules(hub, piscina, () => {
lock2 = true
promises[i++].resolve()
})
const schedule3 = await startPluginSchedules(hub, piscina, () => {
lock3 = true
promises[i++].resolve()
})
await schedule1.stopSchedule()
await promises[1].promise
expect(lock2 || lock3).toBe(true)
if (lock3) {
await schedule3.stopSchedule()
await promises[2].promise
expect(lock2).toBe(true)
await schedule2.stopSchedule()
} else {
await schedule2.stopSchedule()
await promises[2].promise
expect(lock3).toBe(true)
await schedule3.stopSchedule()
}
})
describe('loading the schedule', () => {
let schedule: PluginScheduleControl | null = null
afterEach(async () => {
await schedule?.stopSchedule()
schedule = null
})
test('loads successfully', async () => {
schedule = await startPluginSchedules(hub, piscina)
expect(hub.pluginSchedule).toEqual({
runEveryMinute: [39],
runEveryHour: [],
runEveryDay: [],
})
await resetTestDatabase(`
async function runEveryDay (meta) {
throw new Error('lol')
}
`)
await schedule.reloadSchedule()
expect(hub.pluginSchedule).toEqual({
runEveryMinute: [39],
runEveryHour: [],
runEveryDay: [],
})
const threadsScheduleReady = await piscina.broadcastTask({ task: 'pluginScheduleReady' })
expect(threadsScheduleReady).toEqual(Array.from({ length: workerThreads }).map(() => true))
})
test('node-schedule tasks are created and removed on stop', async () => {
expect(numberOfScheduledJobs()).toEqual(0)
const schedule = await startPluginSchedules(hub, piscina)
expect(numberOfScheduledJobs()).toEqual(3)
nodeSchedule.scheduleJob('1 1 1 1 1', () => 1)
expect(numberOfScheduledJobs()).toEqual(4)
await schedule.stopSchedule()
expect(numberOfScheduledJobs()).toEqual(0)
})
})
})
})