Compare commits

...

3 Commits

Author SHA1 Message Date
3c6d69c343 Simplified selectColumnSizeHeuristic 2021-02-25 11:16:16 -07:00
1ff71d099e Update Dockerfile 2021-02-25 11:16:16 -07:00
ae9016fbf6 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.
2021-02-25 11:16:16 -07:00
10 changed files with 83 additions and 76 deletions

View File

@ -1,7 +1,6 @@
# dev stage
FROM node:14-alpine as dev
WORKDIR /app
RUN apk update && apk add --no-cache python3 make gcc g++
COPY package*.json ./
RUN npm ci
COPY . .
@ -14,21 +13,13 @@ RUN npx tsc && npm prune --production
FROM node:14-alpine
WORKDIR /app
RUN printf "%b" '#!'"/bin/sh\n\
set -e\n\
if [ ! -z \"\$RUN_MIGRATIONS\" ]; then\n\
echo \"Running migrations.\"\n\
npm run knex:migrate:latest\n\
fi\n\
exec \"\$@\"\n" > docker-entrypoint.sh && chmod +x docker-entrypoint.sh
RUN apk add --update --no-cache util-linux
# Copy over production modules and dist folder
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/db ./db
EXPOSE 4000
ENTRYPOINT [ "./docker-entrypoint.sh" ]
CMD [ "node", "dist/main.js" ]

View File

@ -4,7 +4,7 @@ import {
RouterContext,
graphql,
makeExecutableSchema,
} from "./mods";
} from "./mods.js";
export interface ResolversProps {
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 resolvers = mergeResolvers(modules.map((mod) => mod.resolvers));

View File

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

View File

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

View File

@ -33,18 +33,13 @@ type SolutionCallback = (output: DNode[]) => boolean;
type ColumnSelector = (header: CNode) => CNode;
function selectColumnSizeHeuristic(header: CNode): CNode {
let minSize = Infinity;
let minColumn: CNode | undefined;
let curColumn = header;
let minColumn = header.right;
let curColumn = minColumn;
while ((curColumn = curColumn.right) !== header) {
if (curColumn.column.size < minSize) {
minSize = curColumn.column.size;
if (curColumn.size < minColumn.size) {
minColumn = curColumn;
}
}
if (!minColumn) {
throw new Error("minColumn is undefined, this shouldn't be possible.");
}
return minColumn;
}

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 { prettyPrint } from "./util";
import { prettyPrint } from "./util.js";
const TIMEOUT = 20000;
export type Cell = number;
@ -19,46 +20,64 @@ export type Sudoku = {
cells: Cell[];
};
const pool = new StaticPool<GenerateArguments, Cell[]>({
size: WORKERS,
task: "./src/sudoku/worker.js",
});
function getWorker() {
return spawn(new Worker("./worker"));
}
const available: any = [];
function initialize() {
console.log(`Starting ${WORKERS} worker threads`);
for (let n = 0; n < WORKERS; n++) {
getWorker().then((worker) => available.push(worker));
}
}
initialize();
/**
* Awaits a promise with a timeout.
*
* @param promise the promise to await
* @param ms the timeout in milliseconds
* @param cb a callback to call when the timeout is reached. The promise is
* rejected with whatever gets returned here.
*/
function withTimeout<T>(promise: Promise<T>, ms: number, cb: () => any) {
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(
regionWidth: number,
regionHeight: number,
clues: number
): Promise<Sudoku> {
if (activeWorkers >= WORKERS) {
throw new Error("No workers available. Please try again in a moment.");
const proxy = available.pop();
if (!proxy) {
throw new Error("No workers available right now. Please try again.");
}
try {
activeWorkers++;
const puzzle = await pool.exec(
{
regionWidth,
regionHeight,
clues,
},
TIMEOUT
);
prettyPrint(regionWidth, regionHeight, puzzle);
return {
regionWidth,
regionHeight,
size: (regionWidth * regionHeight) ** 2,
cells: puzzle,
};
} catch (err) {
if (isTimeoutError(err)) {
throw new Error("Timed out. Try increasing the number of clues.");
const puzzle = await withTimeout<number[]>(
proxy.generate(regionWidth, regionHeight, clues),
TIMEOUT,
() => {
Thread.terminate(proxy);
getWorker().then((worker) => available.push(worker));
return new Error("Timed out. Try reducing the number of clues.");
}
throw err;
} finally {
activeWorkers--;
}
);
available.push(proxy);
prettyPrint(regionWidth, regionHeight, puzzle);
return {
regionWidth,
regionHeight,
size: (regionWidth * regionHeight) ** 2,
cells: puzzle,
};
}

View File

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

View File

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

View File

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