feature: Adding in the stopping state (#2677)

* feature: Adding in the stopping state

* chore: Deal with timeout in the sigterm for main

* chore: Update the timeout

* Update web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts

Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>

* Update web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts

Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>

---------

Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Jade
2024-07-22 11:40:12 -06:00
committed by GitHub
parent 196561fed2
commit 3eb0093d2a
18 changed files with 282 additions and 156 deletions

View File

@@ -1,3 +1,4 @@
import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk"
import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ImageId, ValidIfNoStupidEscape } from "../types"
@@ -10,6 +11,7 @@ export class CommandController {
readonly runningAnswer: Promise<unknown>,
readonly overlay: Overlay,
readonly pid: number | undefined,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {}
static of<Manifest extends SDKManifest>() {
return async <A extends string>(
@@ -20,6 +22,8 @@ export class CommandController {
},
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: {
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
env?:
@@ -67,10 +71,14 @@ export class CommandController {
const pid = childProcess.pid
return new CommandController(answer, overlay, pid)
return new CommandController(answer, overlay, pid, options.sigtermTimeout)
}
}
async wait() {
async wait(timeout: number = NO_TIMEOUT) {
if (timeout > 0)
setTimeout(() => {
this.term()
}, timeout)
try {
return await this.runningAnswer
} finally {
@@ -82,7 +90,7 @@ export class CommandController {
await this.overlay.destroy().catch((_) => {})
}
}
async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) {
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
if (this.pid === undefined) return
try {
await cpExecFile("pkill", [

View File

@@ -34,6 +34,7 @@ export class Daemon {
user?: string | undefined
onStdout?: (x: Buffer) => void
onStderr?: (x: Buffer) => void
sigtermTimeout?: number
},
) => {
const startCommand = () =>

View File

@@ -44,6 +44,7 @@ type DaemonsParams<
env?: Record<string, string>
ready: Ready
requires: Exclude<Ids, Id>[]
sigtermTimeout?: number
}
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
@@ -136,6 +137,7 @@ export class Daemons<Manifest extends SDKManifest, Ids extends string> {
this.ids,
options.ready,
this.effects,
options.sigtermTimeout,
)
const daemons = this.daemons.concat(daemon)
const ids = [...this.ids, id] as (Ids | Id)[]

View File

@@ -3,6 +3,7 @@ import { defaultTrigger } from "../trigger/defaultTrigger"
import { Ready } from "./Daemons"
import { Daemon } from "./Daemon"
import { Effects } from "../types"
import { DEFAULT_SIGTERM_TIMEOUT } from "."
const oncePromise = <T>() => {
let resolve: (value: T) => void
@@ -32,6 +33,7 @@ export class HealthDaemon {
readonly ids: string[],
readonly ready: Ready,
readonly effects: Effects,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {
this.updateStatus()
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
@@ -46,7 +48,12 @@ export class HealthDaemon {
this.#running = false
this.#healthCheckCleanup?.()
await this.daemon.then((d) => d.stop(termOptions))
await this.daemon.then((d) =>
d.stop({
timeout: this.sigtermTimeout,
...termOptions,
}),
)
}
/** Want to add another notifier that the health might have changed */

View File

@@ -7,6 +7,7 @@ import "./Daemons"
import { SDKManifest } from "../manifest/ManifestTypes"
import { MainEffects } from "../StartSdk"
export const DEFAULT_SIGTERM_TIMEOUT = 30_000
/**
* Used to ensure that the main function is running with the valid proofs.
* We first do the folowing order of things

View File

@@ -1,5 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Duration } from "./Duration"
import type { HealthCheckId } from "./HealthCheckId"
import type { HealthCheckResult } from "./HealthCheckResult"
@@ -7,7 +6,7 @@ export type MainStatus =
| { status: "stopped" }
| { status: "restarting" }
| { status: "restoring" }
| { status: "stopping"; timeout: Duration }
| { status: "stopping" }
| { status: "starting" }
| {
status: "running"

34
sdk/lib/util/inMs.test.ts Normal file
View File

@@ -0,0 +1,34 @@
import { inMs } from "./inMs"
describe("inMs", () => {
test("28.001s", () => {
expect(inMs("28.001s")).toBe(28001)
})
test("28.123s", () => {
expect(inMs("28.123s")).toBe(28123)
})
test(".123s", () => {
expect(inMs(".123s")).toBe(123)
})
test("123ms", () => {
expect(inMs("123ms")).toBe(123)
})
test("1h", () => {
expect(inMs("1h")).toBe(3600000)
})
test("1m", () => {
expect(inMs("1m")).toBe(60000)
})
test("1m", () => {
expect(inMs("1d")).toBe(1000 * 60 * 60 * 24)
})
test("123", () => {
expect(() => inMs("123")).toThrowError("Invalid time format: 123")
})
test("123 as number", () => {
expect(inMs(123)).toBe(123)
})
test.only("undefined", () => {
expect(inMs(undefined)).toBe(undefined)
})
})

31
sdk/lib/util/inMs.ts Normal file
View File

@@ -0,0 +1,31 @@
import { DEFAULT_SIGTERM_TIMEOUT } from "../mainFn"
const matchTimeRegex = /^\s*(\d+)?(\.\d+)?\s*(ms|s|m|h|d)/
const unitMultiplier = (unit?: string) => {
if (!unit) return 1
if (unit === "ms") return 1
if (unit === "s") return 1000
if (unit === "m") return 1000 * 60
if (unit === "h") return 1000 * 60 * 60
if (unit === "d") return 1000 * 60 * 60 * 24
throw new Error(`Invalid unit: ${unit}`)
}
const digitsMs = (digits: string | null, multiplier: number) => {
if (!digits) return 0
const value = parseInt(digits.slice(1))
const divideBy = multiplier / Math.pow(10, digits.length - 1)
return Math.round(value * divideBy)
}
export const inMs = (time?: string | number) => {
if (typeof time === "number") return time
if (!time) return undefined
const matches = time.match(matchTimeRegex)
if (!matches) throw new Error(`Invalid time format: ${time}`)
const [_, leftHandSide, digits, unit] = matches
const multiplier = unitMultiplier(unit)
const firstValue = parseInt(leftHandSide || "0") * multiplier
const secondValue = digitsMs(digits, multiplier)
return firstValue + secondValue
}

View File

@@ -12,3 +12,4 @@ export { addressHostToUrl } from "./getServiceInterface"
export { hostnameInfoToAddress } from "./Hostname"
export * from "./typeHelpers"
export { getDefaultString } from "./getDefaultString"
export { inMs } from "./inMs"