Update all imports to .js for esm support, use threads.js

threads.js has better support for modules - no need to give a
project-relative path to the worker file, which complicated the build.

Add rudimentary thread pooling w/ execution timeout.
This commit is contained in:
Matt Low 2021-02-25 00:34:34 -07:00
parent e54f4c42f1
commit 67ec0d5430
8 changed files with 62 additions and 46 deletions

View File

@ -4,7 +4,7 @@ import {
RouterContext, RouterContext,
graphql, graphql,
makeExecutableSchema, makeExecutableSchema,
} from "./mods"; } from "./mods.js";
export interface ResolversProps { export interface ResolversProps {
Query?: any; Query?: any;

View File

@ -1,6 +1,7 @@
import { mergeTypeDefs, mergeResolvers } from "../mods"; import { mergeTypeDefs, mergeResolvers } from "../mods.js";
import * as sudoku from "./sudoku.js";
const modules = [require("./sudoku")]; const modules = [sudoku];
export const typeDefs = mergeTypeDefs(modules.map((mod) => mod.typeDefs)); export const typeDefs = mergeTypeDefs(modules.map((mod) => mod.typeDefs));
export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers)); export const resolvers = mergeResolvers(modules.map((mod) => mod.resolvers));

View File

@ -1,5 +1,5 @@
import { gql } from "../mods"; import { gql } from "../mods.js";
import { generate, GenerateArguments } from "../sudoku/index"; import { generate, GenerateArguments } from "../sudoku/index.js";
export const typeDefs = gql` export const typeDefs = gql`
""" """

View File

@ -1,6 +1,6 @@
import { Application, bodyParser } from "./mods"; import { Application, bodyParser } from "./mods.js";
import { applyGraphQL } from "./graphql"; import { applyGraphQL } from "./graphql.js";
import { typeDefs, resolvers } from "./graphql/index"; import { typeDefs, resolvers } from "./graphql/index.js";
import stoppable from "stoppable"; import stoppable from "stoppable";
import cors from "@koa/cors"; import cors from "@koa/cors";

View File

@ -1,7 +1,8 @@
import { StaticPool, isTimeoutError } from "node-worker-threads-pool"; import { spawn, Thread, Worker } from "threads";
import WORKERS from "physical-cpu-count"; import WORKERS from "physical-cpu-count";
import { prettyPrint } from "./util"; import { prettyPrint } from "./util.js";
const TIMEOUT = 20000; const TIMEOUT = 20000;
export type Cell = number; export type Cell = number;
@ -19,34 +20,52 @@ export type Sudoku = {
cells: Cell[]; cells: Cell[];
}; };
const pool = new StaticPool<GenerateArguments, Cell[]>({ function getWorker() {
size: WORKERS, return spawn(new Worker("./worker"));
task: "./src/sudoku/worker.js", }
});
const available: any = [];
function initialize() {
for (let n = 0; n < WORKERS; n++) {
getWorker().then((worker) => available.push(worker));
}
}
initialize();
function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => Error) {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => {
reject(cb());
}, ms);
promise.then(resolve).catch(reject);
}).finally(() => clearTimeout(timeout!));
}
let activeWorkers = 0;
export async function generate( export async function generate(
regionWidth: number, regionWidth: number,
regionHeight: number, regionHeight: number,
clues: number clues: number
): Promise<Sudoku> { ): Promise<Sudoku> {
if (activeWorkers >= WORKERS) { const proxy = available.shift();
throw new Error("No workers available. Please try again in a moment."); if (!proxy) {
throw new Error("No workers available right now. Please try again.");
} }
try { try {
activeWorkers++; const puzzle: number[] = await withTimeout(
const puzzle = await pool.exec( proxy.generate(regionWidth, regionHeight, clues),
{ TIMEOUT,
regionWidth, () => {
regionHeight, Thread.terminate(proxy);
clues, getWorker().then((worker) => available.push(worker));
}, return new Error("Timed out. Try reducing the number of clues.");
TIMEOUT }
); );
available.unshift(proxy);
prettyPrint(regionWidth, regionHeight, puzzle); prettyPrint(regionWidth, regionHeight, puzzle);
return { return {
regionWidth, regionWidth,
regionHeight, regionHeight,
@ -54,11 +73,6 @@ export async function generate(
cells: puzzle, cells: puzzle,
}; };
} catch (err) { } catch (err) {
if (isTimeoutError(err)) {
throw new Error("Timed out. Try increasing the number of clues.");
}
throw err; throw err;
} finally {
activeWorkers--;
} }
} }

View File

@ -6,9 +6,9 @@ import {
addNodeToColumn, addNodeToColumn,
maskRow, maskRow,
unmaskRow, unmaskRow,
} from "./dlx"; } from "./dlx.js";
import { shuffle, range } from "./util"; import { shuffle, range } from "./util.js";
import { Cell } from "./index"; import { Cell } from "./index.js";
type NodeMeta = { type NodeMeta = {
index: number; index: number;

View File

@ -1,4 +1,4 @@
import { Cell } from "./index"; import { Cell } from "./index.js";
export function randInt(lower: number, upper: number) { export function randInt(lower: number, upper: number) {
return Math.floor(Math.random() * (upper - lower)) + lower; return Math.floor(Math.random() * (upper - lower)) + lower;

View File

@ -1,15 +1,16 @@
const { SudokuMath } = require("./math"); import { SudokuMath } from "./math.js";
const { parentPort } = require("worker_threads"); import { expose } from "threads/worker";
const maths = {}; const maths = {};
parentPort.on("message", ({ regionWidth, regionHeight, clues }) => { expose({
const math = generate(regionWidth, regionHeight, clues) {
maths[`${regionWidth}:${regionHeight}`] || const math =
(maths[`${regionWidth}:${regionHeight}`] = new SudokuMath( maths[`${regionWidth}:${regionHeight}`] ||
regionWidth, (maths[`${regionWidth}:${regionHeight}`] = new SudokuMath(
regionHeight regionWidth,
)); regionHeight
const puzzle = math.generate(clues); ));
parentPort.postMessage(puzzle); return math.generate(clues);
},
}); });