reduce task leaking (#2868)

* reduce task leaking

* fix onLeaveContext
This commit is contained in:
Aiden McClelland
2025-04-16 11:00:46 -06:00
committed by GitHub
parent 03f8b73627
commit 89f3fdc05f
18 changed files with 159 additions and 98 deletions

View File

@@ -28,7 +28,10 @@ import { UrlString } from "./util/getServiceInterface"
/** Used to reach out from the pure js runtime */
export type Effects = {
child: (name: string) => Effects
constRetry?: () => void
isInContext: boolean
onLeaveContext: (fn: () => void | null | undefined) => void
clearCallbacks: (
options: { only: number[] } | { except: number[] },
) => Promise<null>

View File

@@ -99,7 +99,13 @@ export class Action<
async exportMetadata(options: {
effects: T.Effects
}): Promise<T.ActionMetadata> {
const metadata = await callMaybeFn(this.metadataFn, options)
const childEffects = options.effects.child(`setupActions/${this.id}`)
childEffects.constRetry = once(() => {
this.exportMetadata(options)
})
const metadata = await callMaybeFn(this.metadataFn, {
effects: childEffects,
})
await options.effects.action.export({ id: this.id, metadata })
return metadata
}
@@ -131,12 +137,6 @@ export class Actions<
return new Actions({ ...this.actions, [action.id]: action })
}
async update(options: { effects: T.Effects }): Promise<null> {
options.effects = {
...options.effects,
constRetry: once(() => {
this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem
}),
}
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
}

View File

@@ -40,13 +40,12 @@ export function setupDependencies<Manifest extends T.SDKManifest>(
): (options: { effects: T.Effects }) => Promise<null> {
const cell = { updater: async (_: { effects: T.Effects }) => null }
cell.updater = async (options: { effects: T.Effects }) => {
options.effects = {
...options.effects,
constRetry: once(() => {
cell.updater(options)
}),
}
const dependencyType = await fn(options)
const childEffects = options.effects.child("setupDependencies")
childEffects.constRetry = once(() => {
cell.updater({ effects: options.effects })
})
const dependencyType = await fn({ effects: childEffects })
return await options.effects.setDependencies({
dependencies: Object.entries(dependencyType)
.map(([k, v]) => [k, v as DependencyRequirement] as const)

View File

@@ -28,24 +28,22 @@ export const setupServiceInterfaces: SetupServiceInterfaces = <
[] as any as Output) as UpdateServiceInterfaces<Output>,
}
cell.updater = (async (options: { effects: T.Effects }) => {
options.effects = {
...options.effects,
constRetry: once(() => {
cell.updater(options)
}),
}
const childEffects = options.effects.child("setupInterfaces")
childEffects.constRetry = once(() => {
cell.updater({ effects: options.effects })
})
const bindings: T.BindId[] = []
const interfaces: T.ServiceInterfaceId[] = []
const res = await fn({
effects: {
...options.effects,
...childEffects,
bind: (params: T.BindParams) => {
bindings.push({ id: params.id, internalPort: params.internalPort })
return options.effects.bind(params)
return childEffects.bind(params)
},
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
interfaces.push(params.id)
return options.effects.exportServiceInterface(params)
return childEffects.exportServiceInterface(params)
},
},
})

View File

@@ -46,6 +46,9 @@ type EffectsTypeChecker<T extends StringObject = Effects> = {
describe("startosTypeValidation ", () => {
test(`checking the params match`, () => {
typeEquality<EffectsTypeChecker>({
child: "",
isInContext: {} as never,
onLeaveContext: () => {},
clearCallbacks: {} as ClearCallbacksParams,
action: {
clear: {} as ClearActionsParams,

View File

@@ -25,10 +25,15 @@ export class GetSystemSmtp {
* Watches the system SMTP credentials. Returns an async iterator that yields whenever the value changes
*/
async *watch() {
while (true) {
let callback: () => void
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
yield await this.effects.getSystemSmtp({
callback: () => callback(),

View File

@@ -248,10 +248,15 @@ export class GetServiceInterface {
*/
async *watch() {
const { id, packageId } = this.opts
while (true) {
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
yield await makeInterfaceFilled({
effects: this.effects,

View File

@@ -82,10 +82,15 @@ export class GetServiceInterfaces {
*/
async *watch() {
const { packageId } = this.opts
while (true) {
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
yield await makeManyInterfaceFilled({
effects: this.effects,