import { once } from "node:events";
import pidusage from "pidusage";
import { log, loggerOptions } from "../log.js";
import { Future } from "../utils.js";
const MEMORY_MONITOR_INTERVAL = 5e3;
const MEMORY_WARN_COOLDOWN = 12e4;
const MEMORY_WARN_RESET_DELTA_MB = 50;
class SupervisedProc {
  #opts;
  #started = false;
  #closing = false;
  #startedAt;
  #runningJob = void 0;
  proc;
  #pingInterval;
  #memoryMonitorInterval;
  #pongTimeout;
  memoryBaselineMB;
  #lastMemoryWarnAt = 0;
  #lastMemoryWarnMB = 0;
  init = new Future();
  #join = new Future();
  #logger = log().child({ runningJob: this.#runningJob });
  constructor(initializeTimeout, closeTimeout, memoryWarnMB, memoryLimitMB, pingInterval, pingTimeout, highPingThreshold) {
    this.#opts = {
      initializeTimeout,
      closeTimeout,
      memoryWarnMB,
      memoryLimitMB,
      pingInterval,
      pingTimeout,
      highPingThreshold
    };
  }
  get started() {
    return this.#started;
  }
  get isAlive() {
    var _a;
    return this.#started && !this.#closing && !!((_a = this.proc) == null ? void 0 : _a.connected);
  }
  get runningJob() {
    return this.#runningJob;
  }
  async start() {
    if (this.#started) {
      throw new Error("runner already started");
    } else if (this.#closing) {
      throw new Error("runner is closed");
    }
    this.proc = this.createProcess();
    this.#started = true;
    this.#startedAt = performance.now();
    this.run().catch((err) => {
      this.#logger.child({ err }).warn("supervised process run failed");
      this.#join.resolve();
    });
  }
  async run() {
    await this.init.await;
    this.#pingInterval = setInterval(() => {
      var _a;
      if ((_a = this.proc) == null ? void 0 : _a.connected) {
        this.proc.send({ case: "pingRequest", value: { timestamp: Date.now() } });
      }
    }, this.#opts.pingInterval);
    this.#pongTimeout = setTimeout(() => {
      this.#logger.warn("job is unresponsive");
      clearTimeout(this.#pongTimeout);
      clearInterval(this.#pingInterval);
      this.proc.kill();
      this.#join.resolve();
    }, this.#opts.pingTimeout);
    this.#memoryMonitorInterval = setInterval(async () => {
      const memoryMB = await this.getChildMemoryUsageMB();
      if (memoryMB === 0) {
        return;
      }
      this.memoryBaselineMB ??= memoryMB;
      if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {
        this.#logger.child(this.memoryLoggingFields(memoryMB)).error(`${this.processKind} process exceeded memory limit, killing it`);
        this.close();
      } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {
        if (this.shouldEmitMemoryWarning(memoryMB)) {
          const advisory = this.#opts.memoryLimitMB <= 0;
          this.#logger.child(this.memoryLoggingFields(memoryMB)).warn(
            `${this.processKind} process memory usage is above the warning threshold${advisory ? " (advisory only, the process will not be terminated)" : ""}`
          );
        }
      }
    }, MEMORY_MONITOR_INTERVAL);
    const listener = (msg) => {
      var _a;
      switch (msg.case) {
        case "pongResponse": {
          const delay = Date.now() - msg.value.timestamp;
          if (delay > this.#opts.highPingThreshold) {
            this.#logger.child({ delay }).warn("job executor is unresponsive");
          }
          (_a = this.#pongTimeout) == null ? void 0 : _a.refresh();
          break;
        }
        case "exiting": {
          this.#logger.child({ reason: msg.value.reason }).debug("job exiting");
          break;
        }
        case "done": {
          this.#closing = true;
          this.proc.off("message", listener);
          break;
        }
      }
    };
    this.proc.on("message", listener);
    this.proc.on("error", (err) => {
      if (this.#closing) return;
      this.#logger.child({ err }).warn("job process exited unexpectedly; this likely means the error above caused a crash");
      this.clearTimers();
      this.#join.resolve();
    });
    this.proc.on("exit", () => {
      this.clearTimers();
      this.#join.resolve();
    });
    this.mainTask(this.proc);
    await this.#join.await;
  }
  async join() {
    if (!this.#started) {
      throw new Error("runner not started");
    }
    await this.#join.await;
  }
  async initialize() {
    var _a;
    if (!((_a = this.proc) == null ? void 0 : _a.connected)) {
      const err = new Error("process not connected");
      this.init.reject(err);
      throw err;
    }
    this.proc.send({
      case: "initializeRequest",
      value: {
        loggerOptions: loggerOptions(),
        pingInterval: this.#opts.pingInterval,
        pingTimeout: this.#opts.pingTimeout,
        highPingThreshold: this.#opts.highPingThreshold
      }
    });
    const abort = new AbortController();
    let timer;
    const timeout = new Promise((_, reject) => {
      timer = setTimeout(
        () => reject(new Error("runner initialization timed out")),
        this.#opts.initializeTimeout
      );
    });
    const onMessage = once(this.proc, "message", { signal: abort.signal }).then(
      ([msg]) => {
        if (msg.case !== "initializeResponse") {
          throw new Error("first message must be InitializeResponse");
        }
      }
    );
    const onExit = once(this.proc, "exit", { signal: abort.signal }).then(([code, signal]) => {
      throw new Error(`process exited before initializing (code ${code}, signal ${signal})`);
    });
    try {
      await Promise.race([onMessage, onExit, timeout]);
      this.init.resolve();
    } catch (err) {
      this.init.reject(err);
      if (this.proc && this.proc.exitCode === null && !this.proc.killed) {
        this.proc.kill();
      }
      throw err;
    } finally {
      clearTimeout(timer);
      abort.abort();
      onMessage.catch(() => {
      });
      onExit.catch(() => {
      });
    }
  }
  async close() {
    var _a;
    if (!this.#started) {
      return;
    }
    this.#closing = true;
    if ((_a = this.proc) == null ? void 0 : _a.connected) {
      this.proc.send({ case: "shutdownRequest" });
    }
    const timer = setTimeout(() => {
      this.#logger.error("job shutdown is taking too much time");
      this.proc.kill();
    }, this.#opts.closeTimeout);
    await this.#join.await.then(() => {
      clearTimeout(timer);
      this.clearTimers();
    });
  }
  async launchJob(info) {
    var _a;
    if (this.#runningJob) {
      throw new Error("executor already has a running job");
    }
    if (!((_a = this.proc) == null ? void 0 : _a.connected)) {
      throw new Error("process not connected");
    }
    this.#runningJob = info;
    this.proc.send({ case: "startJobRequest", value: { runningJob: info } });
  }
  async getChildMemoryUsageMB() {
    var _a;
    const pid = (_a = this.proc) == null ? void 0 : _a.pid;
    if (!pid) {
      return 0;
    }
    try {
      const stats = await pidusage(pid);
      return stats.memory / (1024 * 1024);
    } catch (err) {
      const code = err.code;
      if (code === "ENOENT" || code === "ESRCH") {
        return 0;
      }
      throw err;
    }
  }
  get uptime() {
    if (this.#startedAt === void 0) {
      return 0;
    }
    return performance.now() - this.#startedAt;
  }
  shouldEmitMemoryWarning(memoryMB, now = performance.now()) {
    const cooledDown = now - this.#lastMemoryWarnAt >= MEMORY_WARN_COOLDOWN;
    const grew = memoryMB - this.#lastMemoryWarnMB >= MEMORY_WARN_RESET_DELTA_MB;
    if (cooledDown || grew) {
      this.#lastMemoryWarnAt = now;
      this.#lastMemoryWarnMB = memoryMB;
      return true;
    }
    return false;
  }
  memoryLoggingFields(memoryMB) {
    var _a;
    const fields = {
      pid: (_a = this.proc) == null ? void 0 : _a.pid,
      memoryUsageMB: Math.round(memoryMB * 10) / 10,
      memoryWarnMB: this.#opts.memoryWarnMB,
      memoryLimitMB: this.#opts.memoryLimitMB,
      uptime: this.uptime,
      hasRunningJob: this.runningJob !== void 0
    };
    if (this.memoryBaselineMB !== void 0) {
      fields.baselineMemoryMB = Math.round(this.memoryBaselineMB * 10) / 10;
      fields.growthMemoryMB = Math.round((memoryMB - this.memoryBaselineMB) * 10) / 10;
    }
    return fields;
  }
  clearTimers() {
    clearTimeout(this.#pongTimeout);
    clearInterval(this.#pingInterval);
    clearInterval(this.#memoryMonitorInterval);
  }
}
export {
  SupervisedProc
};
//# sourceMappingURL=supervised_proc.js.map