Skip to content

Commit

Permalink
Prepare 1.0.0-alpha-4
Browse files Browse the repository at this point in the history
Change process option 'name' to 'id'.

Add option 'overrun'. Add option 'timeout'.

Refactor process status tracking.
  • Loading branch information
Hexagon committed Mar 12, 2023
1 parent eb3cf56 commit 5e0be42
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 233 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ Here's an example of a `pup.jsonc` with all possible options defined:
"TZ": "Europe/Olso"
},
"autostart": true, // default undefined, process will not autostart by default
"overrun": false, // allow overrun, default false
// "cron": "*/5 * * * * *", // default undefined
"restart": "always", // default undefined, possible values ["always" | "error" | undefined]
"maxRestarts": 10, // default undefined - restart infinitely'
"restartDelayMs": 10000 // default 10000

"restartDelayMs": 10000, // default 10000
// Only needed if you want to overrides the global logger
// Note: "colors" is not configurable per process
"logger": {
Expand All @@ -94,8 +94,8 @@ Here's an example of a `pup.jsonc` with all possible options defined:
}
```

In this example, we define a process called `server-task`. We specify the command to start the process using an array of strings. We set it to start immediately with, and to restart after 10 seconds after
quitting for whatever reason.
In this example, we define a process called `server-task`. We specify the command to start the process using an array of strings. We set it to start immediately with, and to restart after 10 seconds
after quitting for whatever reason.

If you use the line `cron: "<pattern>"` instead of `autostart: true` it would be triggered periodically.

Expand Down
2 changes: 1 addition & 1 deletion examples/max-restarts/pup.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"autostart": true,
"restart": "always",
"maxRestarts": 3,
"restartDelayMs": 1000
"restartDelayMs": 3000
}
]
}
2 changes: 1 addition & 1 deletion examples/minimal/server.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
console.log("I will restart forever")
console.log("My process id is ", Deno.env.get("PUP_PROCESS_ID"));
console.log("My process id is ", Deno.env.get("PUP_PROCESS_ID"))
4 changes: 2 additions & 2 deletions lib/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ async function fileExists(filePath: string) {
}
}

function isRunning(pid: number, heartbeat: number, thresholdMs: number): string {
function isRunning(pid: number, heartbeat: Date, thresholdMs: number): string {
try {
Deno.kill(pid, "SIGURG")
return "Running"
} catch (e) {
if (e.name === "TypeError" || e.name === "PermissionDenied") {
if (heartbeat) {
return (new Date().getTime() - heartbeat) < thresholdMs ? "Running" : "Unknown"
return (new Date().getTime() - heartbeat.getTime()) < thresholdMs ? "Running" : "Unknown"
} else {
return "Not running"
}
Expand Down
18 changes: 17 additions & 1 deletion lib/core/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { z } from "../../deps.ts"
interface Configuration {
logger?: GlobalLoggerConfiguration
processes: ProcessConfiguration[]
/* plugins?: PluginEntry[] */
}

/*interface PluginEntry {
url: string
options?: unknown
}*/

interface _BaseLoggerConfiguration {
console?: boolean
stdout?: string
Expand All @@ -28,11 +34,13 @@ interface ProcessConfiguration {
env?: Record<string, string>
cwd?: string
autostart?: boolean
overrun?: boolean
cron?: string
maxRestarts?: number
restart?: string
restartDelayMs?: number
logger?: ProcessLoggerConfiguration
timeout?: number
}

const ConfigurationSchema = z.object({
Expand All @@ -46,6 +54,12 @@ const ConfigurationSchema = z.object({
decorate: z.optional(z.boolean()),
}).strict(),
),
/*plugins: z.optional(
z.object({
url: z.string(),
options: z.optional(z.object({}))
}).strict()
),*/
processes: z.array(
z.object({
id: z.string().min(1).max(64).regex(/^[a-z0-9@._\-]+$/i, "Process ID can only contain characters a-Z 0-9 . _ - or @"),
Expand All @@ -55,8 +69,10 @@ const ConfigurationSchema = z.object({
autostart: z.optional(z.boolean()),
cron: z.optional(z.string().min(9).max(256)),
restart: z.optional(z.enum(["always", "error"])),
restartDelayMs: z.optional(z.number().min(0).max(24*60*60*1000*1)), // Max one day
restartDelayMs: z.optional(z.number().min(0).max(24 * 60 * 60 * 1000 * 1)), // Max one day
overrun: z.optional(z.boolean()),
maxRestarts: z.optional(z.number().min(0)),
timeout: z.optional(z.number().min(1)),
logger: z.optional(
z.object({
console: z.optional(z.boolean()),
Expand Down
2 changes: 1 addition & 1 deletion lib/core/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Logger {
if (blockedByAttachedLogger) return

// Default initiator to
const initiator = process?.name || "core"
const initiator = process?.id || "core"

// Log to console
const logToConsoleProcess = (process?.logger?.console ?? true) === false
Expand Down
209 changes: 209 additions & 0 deletions lib/core/process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { Pup } from "./pup.ts"
import { Cron } from "../../deps.ts"
import { Runner } from "./runner.ts"
import { ProcessConfiguration } from "./configuration.ts"

enum ProcessStatus {
CREATED = 0,
STARTING = 100,
RUNNING = 200,
STOPPING = 250,
FINISHED = 300,
ERRORED = 400,
EXHAUSTED = 450,
BLOCKED = 500,
}

interface ProcessInformation {
id: string
status: ProcessStatus
code?: number
signal?: number
pid?: number
started?: Date
exited?: Date
blocked?: boolean
restarts?: number
updated: Date
}

interface ProcessInformationParsed {
id: string
status: ProcessStatus
code?: number
signal?: number
pid?: number
started?: string
exited?: string
blocked?: boolean
restarts?: number
updated: string
}

class Process {
private readonly config: ProcessConfiguration
private readonly pup: Pup

// Subprocess runner
private runner?: Runner

// Allow manual block
private blocked = false

// Status
private status: ProcessStatus = ProcessStatus.CREATED
private pid?: number
private code?: number
private signal?: number
private started?: Date
private exited?: Date
private restarts = 0
private updated: Date = new Date()

constructor(pup: Pup, config: ProcessConfiguration) {
this.config = config
this.pup = pup
}

private setStatus(s: ProcessStatus) {
this.status = s
this.updated = new Date()
}

public getStatus(): ProcessInformation {
return {
id: this.config.id,
status: this.status,
pid: this.pid,
code: this.code,
signal: this.signal,
started: this.started,
exited: this.exited,
blocked: this.blocked,
restarts: this.restarts,
updated: this.updated,
}
}

public getConfig() {
return this.config
}

public init = () => {
// Start using cron pattern
if (this.config.cron) this.setupCron()
}

public start = async (reason?: string, restart?: boolean) => {
const logger = this.pup.logger

// Do not start if blocked
if (this.blocked) {
logger.log("blocked", `Process blocked, refusing to start`, this.config)
return
}

// Do not start if running and overrun isn't enabled
if (this.status === ProcessStatus.RUNNING && !this.config.overrun) {
logger.log("blocked", `Process still running, refusing to start`, this.config)
return
}

// Do not restart if maximum number of restarts are exhausted
if (this.restarts >= (this.config.maxRestarts ?? Infinity)) {
logger.log("exhausted", `Maximum number of starts exhausted, refusing to start`, this.config)
this.setStatus(ProcessStatus.EXHAUSTED)
return
}

logger.log("starting", `Process starting, reason: ${reason}`, this.config)

// Update status
this.setStatus(ProcessStatus.STARTING)
this.pid = undefined
this.code = undefined
this.signal = undefined
this.exited = undefined
this.started = undefined

// Start process (await for it to exit)
this.runner = new Runner(this.pup, this.config)

// Update restart counter, this is reset on successful exit, or manual .stop()
if (restart) {
this.restarts = this.restarts + 1
}

// Try to start
try {
const result = await this.runner.run((pid: number) => {
// Process started
this.setStatus(ProcessStatus.RUNNING)
this.pid = pid
this.started = new Date()
})

this.code = result.code
this.signal = result.signal

// Exited - Update status
if (result.code === 0) {
// Reset restarts on successful exit
this.setStatus(ProcessStatus.FINISHED)
logger.log("finished", `Process finished with code ${result.code}`, this.config)
} else {
this.setStatus(ProcessStatus.ERRORED)
logger.log("errored", `Process exited with code ${result.code}`, this.config)
}
} catch (e) {
this.code = undefined
this.signal = undefined
this.setStatus(ProcessStatus.ERRORED)
logger.log("errored", `Process could not start, error: ${e}`, this.config)
}

this.exited = new Date()
this.pid = undefined
this.runner = undefined
}

public stop = (reason: string): boolean => {
if (this.runner) {
try {
this.pup.logger.log("starting", `Killing process, reason: ${reason}`, this.config)
this.runner?.kill("SIGINT")
this.restarts = 0
return true
} catch (_e) {
return false
}
}
return false
}

public block = () => {
this.blocked = true
}

public unblock = () => {
this.blocked = false
}

private setupCron = () => {
try {
// ToDo: Take care of env TZ?
const cronJob = new Cron(this.config.cron as string, () => {
this.start("Cron pattern")
this.pup.logger.log("scheduler", `${this.config.id} is scheduled to run at '${this.config.cron} (${cronJob.nextRun()?.toLocaleString()})'`)
})

// Initial next run time
this.pup.logger.log("scheduler", `${this.config.id} is scheduled to run at '${this.config.cron} (${cronJob.nextRun()?.toLocaleString()})'`)
} catch (e) {
this.pup.logger.error("scheduled", `Fatal error setup up the cron job for '${this.config.id}', process will not autostart. Error: ${e}`)
}
}
}

export { Process, ProcessStatus }
export type { ProcessInformation, ProcessInformationParsed }
Loading

0 comments on commit 5e0be42

Please sign in to comment.