@swapai/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/effect.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +874 -155
- package/dist/index.js.map +1 -1
- package/dist/{types-DTbU1MEi.d.ts → types-DkvGbscw.d.ts} +1 -0
- package/package.json +1 -1
- package/python/swapai_worker.py +75 -43
package/dist/index.js
CHANGED
|
@@ -128,11 +128,12 @@ import {
|
|
|
128
128
|
mkdir,
|
|
129
129
|
readFile,
|
|
130
130
|
readdir,
|
|
131
|
+
rm,
|
|
131
132
|
stat,
|
|
132
133
|
writeFile
|
|
133
134
|
} from "fs/promises";
|
|
134
135
|
import { createInterface } from "readline";
|
|
135
|
-
import { dirname, join } from "path";
|
|
136
|
+
import { dirname, join, resolve, sep } from "path";
|
|
136
137
|
import { fileURLToPath } from "url";
|
|
137
138
|
|
|
138
139
|
// src/needle.ts
|
|
@@ -224,8 +225,8 @@ var ManagedNeedleRuntime = class {
|
|
|
224
225
|
#environment;
|
|
225
226
|
#closed = false;
|
|
226
227
|
constructor(options) {
|
|
227
|
-
this.#dataDirectory = options.dataDirectory;
|
|
228
|
-
this.#runtimeDirectory = join(
|
|
228
|
+
this.#dataDirectory = resolve(options.dataDirectory);
|
|
229
|
+
this.#runtimeDirectory = join(this.#dataDirectory, "runtime");
|
|
229
230
|
this.#runCommand = options.dependencies?.runCommand ?? runCommand;
|
|
230
231
|
this.#spawnProcess = options.dependencies?.spawnProcess ?? spawnProcess;
|
|
231
232
|
this.#fetch = options.dependencies?.fetch ?? globalThis.fetch;
|
|
@@ -252,6 +253,12 @@ var ManagedNeedleRuntime = class {
|
|
|
252
253
|
"Needle generation must be a non-negative integer."
|
|
253
254
|
);
|
|
254
255
|
}
|
|
256
|
+
if (!Number.isSafeInteger(options.expectedEpoch) || options.expectedEpoch < 0) {
|
|
257
|
+
throw new NeedleRuntimeError(
|
|
258
|
+
"service_unavailable",
|
|
259
|
+
"Needle data epoch must be a non-negative integer."
|
|
260
|
+
);
|
|
261
|
+
}
|
|
255
262
|
if (options.examples.length === 0) {
|
|
256
263
|
throw new NeedleRuntimeError(
|
|
257
264
|
"service_unavailable",
|
|
@@ -281,8 +288,11 @@ var ManagedNeedleRuntime = class {
|
|
|
281
288
|
const lines = options.examples.map(
|
|
282
289
|
(example) => createTrainingLine(example.input, example.result, options.resultConfig)
|
|
283
290
|
);
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
const lockPath = join(
|
|
292
|
+
this.#dataDirectory,
|
|
293
|
+
"locks",
|
|
294
|
+
`${createHash("sha256").update(options.classifierName).digest("hex")}.sqlite`
|
|
295
|
+
);
|
|
286
296
|
try {
|
|
287
297
|
await this.#runCommand(
|
|
288
298
|
this.#pythonPath,
|
|
@@ -296,12 +306,22 @@ var ManagedNeedleRuntime = class {
|
|
|
296
306
|
"--checkpoint-dir",
|
|
297
307
|
checkpointDirectory,
|
|
298
308
|
"--epochs",
|
|
299
|
-
String(options.epochs ?? 10)
|
|
309
|
+
String(options.epochs ?? 10),
|
|
310
|
+
"--artifact-lock-database",
|
|
311
|
+
lockPath,
|
|
312
|
+
"--main-database",
|
|
313
|
+
join(this.#dataDirectory, "swapai.sqlite"),
|
|
314
|
+
"--classifier-name",
|
|
315
|
+
options.classifierName,
|
|
316
|
+
"--expected-epoch",
|
|
317
|
+
String(options.expectedEpoch)
|
|
300
318
|
],
|
|
301
319
|
{
|
|
302
320
|
cwd: candidateDirectory,
|
|
303
321
|
env: this.#environment,
|
|
304
|
-
timeoutMs: 6 * 60 * 60 * 1e3
|
|
322
|
+
timeoutMs: 6 * 60 * 60 * 1e3,
|
|
323
|
+
stdin: `${lines.join("\n")}
|
|
324
|
+
`
|
|
305
325
|
}
|
|
306
326
|
);
|
|
307
327
|
await stat(modelPath);
|
|
@@ -352,6 +372,70 @@ var ManagedNeedleRuntime = class {
|
|
|
352
372
|
throw toRuntimeError("Needle model could not start.", error);
|
|
353
373
|
}
|
|
354
374
|
}
|
|
375
|
+
async clearClassifierArtifacts(classifierName) {
|
|
376
|
+
await rm(
|
|
377
|
+
join(this.#dataDirectory, "classifiers", classifierKey(classifierName)),
|
|
378
|
+
{ recursive: true, force: true }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
async clearClassifierGenerationArtifacts(classifierName, generation) {
|
|
382
|
+
if (!Number.isSafeInteger(generation) || generation < 0) {
|
|
383
|
+
throw new NeedleRuntimeError(
|
|
384
|
+
"service_unavailable",
|
|
385
|
+
"Classifier generation must be a non-negative safe integer."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const classifierDirectory = resolve(
|
|
389
|
+
this.#dataDirectory,
|
|
390
|
+
"classifiers",
|
|
391
|
+
classifierKey(classifierName)
|
|
392
|
+
);
|
|
393
|
+
const generationDirectory = resolve(
|
|
394
|
+
classifierDirectory,
|
|
395
|
+
`generation-${generation}`
|
|
396
|
+
);
|
|
397
|
+
if (!generationDirectory.startsWith(`${classifierDirectory}${sep}`)) {
|
|
398
|
+
throw new NeedleRuntimeError(
|
|
399
|
+
"service_unavailable",
|
|
400
|
+
"Classifier generation path is outside its classifier directory."
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
await rm(generationDirectory, { recursive: true, force: true });
|
|
404
|
+
}
|
|
405
|
+
async clearClassifierArtifactsThroughGeneration(classifierName, maximumGeneration) {
|
|
406
|
+
if (!Number.isSafeInteger(maximumGeneration) || maximumGeneration < 0) {
|
|
407
|
+
throw new NeedleRuntimeError(
|
|
408
|
+
"service_unavailable",
|
|
409
|
+
"Maximum classifier generation must be a non-negative safe integer."
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const classifierDirectory = join(
|
|
413
|
+
this.#dataDirectory,
|
|
414
|
+
"classifiers",
|
|
415
|
+
classifierKey(classifierName)
|
|
416
|
+
);
|
|
417
|
+
let entries;
|
|
418
|
+
try {
|
|
419
|
+
entries = await readdir(classifierDirectory, { withFileTypes: true });
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
await Promise.all(
|
|
427
|
+
entries.map(async (entry) => {
|
|
428
|
+
const match = /^generation-(\d+)$/.exec(entry.name);
|
|
429
|
+
if (!entry.isDirectory() || match === null) return;
|
|
430
|
+
const generation = Number(match[1]);
|
|
431
|
+
if (generation > maximumGeneration) return;
|
|
432
|
+
await rm(join(classifierDirectory, entry.name), {
|
|
433
|
+
recursive: true,
|
|
434
|
+
force: true
|
|
435
|
+
});
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
}
|
|
355
439
|
async close() {
|
|
356
440
|
if (this.#closed) return;
|
|
357
441
|
this.#closed = true;
|
|
@@ -533,12 +617,12 @@ var NeedleModelProcess = class {
|
|
|
533
617
|
this.#resultConfig = resultConfig;
|
|
534
618
|
this.#onBackgroundError = onBackgroundError;
|
|
535
619
|
this.#onClose = onClose;
|
|
536
|
-
this.#readyPromise = new Promise((
|
|
537
|
-
this.#resolveReady =
|
|
620
|
+
this.#readyPromise = new Promise((resolve2, reject) => {
|
|
621
|
+
this.#resolveReady = resolve2;
|
|
538
622
|
this.#rejectReady = reject;
|
|
539
623
|
});
|
|
540
|
-
this.#exitPromise = new Promise((
|
|
541
|
-
this.#resolveExit =
|
|
624
|
+
this.#exitPromise = new Promise((resolve2) => {
|
|
625
|
+
this.#resolveExit = resolve2;
|
|
542
626
|
});
|
|
543
627
|
this.#listen();
|
|
544
628
|
}
|
|
@@ -556,8 +640,8 @@ var NeedleModelProcess = class {
|
|
|
556
640
|
);
|
|
557
641
|
}
|
|
558
642
|
const id = ++this.#requestId;
|
|
559
|
-
const result = new Promise((
|
|
560
|
-
this.#pending.set(id, { resolve, reject });
|
|
643
|
+
const result = new Promise((resolve2, reject) => {
|
|
644
|
+
this.#pending.set(id, { resolve: resolve2, reject });
|
|
561
645
|
this.#process.stdin.write(
|
|
562
646
|
`${JSON.stringify({ id, type: "classify", input })}
|
|
563
647
|
`,
|
|
@@ -596,10 +680,7 @@ var NeedleModelProcess = class {
|
|
|
596
680
|
await withTimeout(this.#exitPromise, 2e3, "Needle ignored SIGTERM.");
|
|
597
681
|
} catch {
|
|
598
682
|
this.#process.kill("SIGKILL");
|
|
599
|
-
await
|
|
600
|
-
this.#exitPromise,
|
|
601
|
-
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
602
|
-
]);
|
|
683
|
+
await withTimeout(this.#exitPromise, 2e3, "Needle ignored SIGKILL.");
|
|
603
684
|
}
|
|
604
685
|
}
|
|
605
686
|
}
|
|
@@ -710,12 +791,16 @@ var spawnProcess = (command, args, options) => spawn(command, [...args], {
|
|
|
710
791
|
env: options.env,
|
|
711
792
|
stdio: ["pipe", "pipe", "pipe"]
|
|
712
793
|
});
|
|
713
|
-
var runCommand = (command, args, options) => new Promise((
|
|
794
|
+
var runCommand = (command, args, options) => new Promise((resolve2, reject) => {
|
|
714
795
|
const child = spawn(command, [...args], {
|
|
715
796
|
cwd: options?.cwd,
|
|
716
797
|
env: options?.env,
|
|
717
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
798
|
+
stdio: [options?.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
|
|
718
799
|
});
|
|
800
|
+
if (options?.stdin !== void 0 && child.stdin !== null) {
|
|
801
|
+
child.stdin.on("error", () => void 0);
|
|
802
|
+
child.stdin.end(options.stdin, "utf8");
|
|
803
|
+
}
|
|
719
804
|
let stdout = "";
|
|
720
805
|
let stderr = "";
|
|
721
806
|
let timedOut = false;
|
|
@@ -764,7 +849,7 @@ var runCommand = (command, args, options) => new Promise((resolve, reject) => {
|
|
|
764
849
|
return;
|
|
765
850
|
}
|
|
766
851
|
if (code === 0) {
|
|
767
|
-
|
|
852
|
+
resolve2({ stdout, stderr });
|
|
768
853
|
return;
|
|
769
854
|
}
|
|
770
855
|
reject(
|
|
@@ -831,7 +916,7 @@ async function findFile(directory, name) {
|
|
|
831
916
|
return void 0;
|
|
832
917
|
}
|
|
833
918
|
function withTimeout(promise, milliseconds, message, onTimeout) {
|
|
834
|
-
return new Promise((
|
|
919
|
+
return new Promise((resolve2, reject) => {
|
|
835
920
|
const timer = setTimeout(() => {
|
|
836
921
|
onTimeout?.();
|
|
837
922
|
reject(new NeedleRuntimeError("service_unavailable", message));
|
|
@@ -840,7 +925,7 @@ function withTimeout(promise, milliseconds, message, onTimeout) {
|
|
|
840
925
|
promise.then(
|
|
841
926
|
(value) => {
|
|
842
927
|
clearTimeout(timer);
|
|
843
|
-
|
|
928
|
+
resolve2(value);
|
|
844
929
|
},
|
|
845
930
|
(error) => {
|
|
846
931
|
clearTimeout(timer);
|
|
@@ -879,7 +964,12 @@ var SCHEMA = `
|
|
|
879
964
|
training_lease_owner TEXT,
|
|
880
965
|
training_lease_until INTEGER,
|
|
881
966
|
created_at INTEGER NOT NULL,
|
|
882
|
-
updated_at INTEGER NOT NULL
|
|
967
|
+
updated_at INTEGER NOT NULL,
|
|
968
|
+
data_epoch INTEGER NOT NULL DEFAULT 0,
|
|
969
|
+
clear_pending INTEGER NOT NULL DEFAULT 0,
|
|
970
|
+
clear_erased INTEGER NOT NULL DEFAULT 0,
|
|
971
|
+
clear_artifact_generation_max INTEGER,
|
|
972
|
+
training_lease_epoch INTEGER
|
|
883
973
|
);
|
|
884
974
|
|
|
885
975
|
CREATE TABLE IF NOT EXISTS generations (
|
|
@@ -911,6 +1001,43 @@ var SCHEMA = `
|
|
|
911
1001
|
ON examples(classifier_name, generation, id);
|
|
912
1002
|
CREATE INDEX IF NOT EXISTS examples_by_split
|
|
913
1003
|
ON examples(classifier_name, generation, split, id);
|
|
1004
|
+
|
|
1005
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_classifiers_insert
|
|
1006
|
+
BEFORE INSERT ON classifiers
|
|
1007
|
+
WHEN swapai_writer_version() < 2
|
|
1008
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1009
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_classifiers_update
|
|
1010
|
+
BEFORE UPDATE ON classifiers
|
|
1011
|
+
WHEN swapai_writer_version() < 2
|
|
1012
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1013
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_classifiers_delete
|
|
1014
|
+
BEFORE DELETE ON classifiers
|
|
1015
|
+
WHEN swapai_writer_version() < 2
|
|
1016
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1017
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_generations_insert
|
|
1018
|
+
BEFORE INSERT ON generations
|
|
1019
|
+
WHEN swapai_writer_version() < 2
|
|
1020
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1021
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_generations_update
|
|
1022
|
+
BEFORE UPDATE ON generations
|
|
1023
|
+
WHEN swapai_writer_version() < 2
|
|
1024
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1025
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_generations_delete
|
|
1026
|
+
BEFORE DELETE ON generations
|
|
1027
|
+
WHEN swapai_writer_version() < 2
|
|
1028
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1029
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_examples_insert
|
|
1030
|
+
BEFORE INSERT ON examples
|
|
1031
|
+
WHEN swapai_writer_version() < 2
|
|
1032
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1033
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_examples_update
|
|
1034
|
+
BEFORE UPDATE ON examples
|
|
1035
|
+
WHEN swapai_writer_version() < 2
|
|
1036
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
1037
|
+
CREATE TRIGGER IF NOT EXISTS swapai_v2_examples_delete
|
|
1038
|
+
BEFORE DELETE ON examples
|
|
1039
|
+
WHEN swapai_writer_version() < 2
|
|
1040
|
+
BEGIN SELECT RAISE(ABORT, 'SwapAI writer is too old'); END;
|
|
914
1041
|
`;
|
|
915
1042
|
function assignExampleSplit(name, input) {
|
|
916
1043
|
const hash = createHash2("sha256").update(name).update("\0").update(input).digest();
|
|
@@ -924,40 +1051,85 @@ function openStorage(options) {
|
|
|
924
1051
|
chmodSync(options.dataDirectory, 448);
|
|
925
1052
|
const databasePath = join2(options.dataDirectory, "swapai.sqlite");
|
|
926
1053
|
const database = new DatabaseSync(databasePath);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const configJson = stringifyJson(options.config, "classifier config");
|
|
934
|
-
const existing = database.prepare(`
|
|
935
|
-
SELECT config_json FROM classifiers WHERE name = ?
|
|
936
|
-
`).get(options.name);
|
|
937
|
-
if (existing !== void 0 && criticalConfigJson(existing.config_json) !== criticalConfigJson(configJson)) {
|
|
938
|
-
database.close();
|
|
939
|
-
throw new TypeError(
|
|
940
|
-
`Classifier "${options.name}" already exists with different result or behavior settings`
|
|
1054
|
+
let artifactLockDatabase = null;
|
|
1055
|
+
try {
|
|
1056
|
+
database.function(
|
|
1057
|
+
"swapai_writer_version",
|
|
1058
|
+
{ deterministic: true },
|
|
1059
|
+
() => 2
|
|
941
1060
|
);
|
|
1061
|
+
chmodSync(databasePath, 384);
|
|
1062
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
1063
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1064
|
+
database.exec("PRAGMA secure_delete = ON");
|
|
1065
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1066
|
+
const secureDelete = requiredRow(
|
|
1067
|
+
database.prepare("PRAGMA secure_delete").get()
|
|
1068
|
+
);
|
|
1069
|
+
if (secureDelete.secure_delete !== 1) {
|
|
1070
|
+
throw new Error("SQLite secure deletion could not be enabled");
|
|
1071
|
+
}
|
|
1072
|
+
database.exec(SCHEMA);
|
|
1073
|
+
migrateClassifierColumns(database);
|
|
1074
|
+
const lockDirectory = join2(options.dataDirectory, "locks");
|
|
1075
|
+
mkdirSync(lockDirectory, { recursive: true, mode: 448 });
|
|
1076
|
+
chmodSync(lockDirectory, 448);
|
|
1077
|
+
const lockPath = join2(
|
|
1078
|
+
lockDirectory,
|
|
1079
|
+
`${createHash2("sha256").update(options.name).digest("hex")}.sqlite`
|
|
1080
|
+
);
|
|
1081
|
+
artifactLockDatabase = new DatabaseSync(lockPath);
|
|
1082
|
+
chmodSync(lockPath, 384);
|
|
1083
|
+
artifactLockDatabase.exec("PRAGMA busy_timeout = 0");
|
|
1084
|
+
artifactLockDatabase.exec(
|
|
1085
|
+
"CREATE TABLE IF NOT EXISTS artifact_lock (id INTEGER PRIMARY KEY)"
|
|
1086
|
+
);
|
|
1087
|
+
const now = Date.now();
|
|
1088
|
+
const configJson = stringifyJson(options.config, "classifier config");
|
|
1089
|
+
const existing = database.prepare(`
|
|
1090
|
+
SELECT config_json FROM classifiers WHERE name = ?
|
|
1091
|
+
`).get(options.name);
|
|
1092
|
+
if (existing !== void 0 && criticalConfigJson(existing.config_json) !== criticalConfigJson(configJson)) {
|
|
1093
|
+
throw new TypeError(
|
|
1094
|
+
`Classifier "${options.name}" already exists with different result or behavior settings`
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
transaction(database, () => {
|
|
1098
|
+
database.prepare(`
|
|
1099
|
+
INSERT INTO classifiers (
|
|
1100
|
+
name, config_json, max_training_set, created_at, updated_at
|
|
1101
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
1102
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
1103
|
+
config_json = excluded.config_json,
|
|
1104
|
+
max_training_set = excluded.max_training_set,
|
|
1105
|
+
updated_at = excluded.updated_at
|
|
1106
|
+
`).run(options.name, configJson, options.maxTrainingSet, now, now);
|
|
1107
|
+
database.prepare(`
|
|
1108
|
+
INSERT OR IGNORE INTO generations (
|
|
1109
|
+
classifier_name, generation, status, created_at
|
|
1110
|
+
) VALUES (?, 1, 'active', ?)
|
|
1111
|
+
`).run(options.name, now);
|
|
1112
|
+
trimExamples(
|
|
1113
|
+
database,
|
|
1114
|
+
options.name,
|
|
1115
|
+
activeGeneration(database, options.name),
|
|
1116
|
+
options.maxTrainingSet
|
|
1117
|
+
);
|
|
1118
|
+
});
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
try {
|
|
1121
|
+
artifactLockDatabase?.close();
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
database.close();
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
throw error;
|
|
942
1129
|
}
|
|
943
|
-
|
|
944
|
-
database.prepare(`
|
|
945
|
-
INSERT INTO classifiers (
|
|
946
|
-
name, config_json, max_training_set, created_at, updated_at
|
|
947
|
-
) VALUES (?, ?, ?, ?, ?)
|
|
948
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
949
|
-
config_json = excluded.config_json,
|
|
950
|
-
max_training_set = excluded.max_training_set,
|
|
951
|
-
updated_at = excluded.updated_at
|
|
952
|
-
`).run(options.name, configJson, options.maxTrainingSet, now, now);
|
|
953
|
-
database.prepare(`
|
|
954
|
-
INSERT OR IGNORE INTO generations (
|
|
955
|
-
classifier_name, generation, status, created_at
|
|
956
|
-
) VALUES (?, 1, 'active', ?)
|
|
957
|
-
`).run(options.name, now);
|
|
958
|
-
trimExamples(database, options.name, activeGeneration(database, options.name), options.maxTrainingSet);
|
|
959
|
-
});
|
|
1130
|
+
const initializedArtifactLockDatabase = artifactLockDatabase;
|
|
960
1131
|
let closed = false;
|
|
1132
|
+
let artifactLockHeld = false;
|
|
961
1133
|
const storage = {
|
|
962
1134
|
databasePath,
|
|
963
1135
|
snapshot() {
|
|
@@ -978,6 +1150,13 @@ function openStorage(options) {
|
|
|
978
1150
|
g.trained,
|
|
979
1151
|
g.model_path,
|
|
980
1152
|
g.needle_version,
|
|
1153
|
+
c.data_epoch,
|
|
1154
|
+
c.clear_pending,
|
|
1155
|
+
c.clear_erased,
|
|
1156
|
+
c.clear_artifact_generation_max,
|
|
1157
|
+
c.training_lease_owner,
|
|
1158
|
+
c.training_lease_epoch,
|
|
1159
|
+
c.training_lease_until,
|
|
981
1160
|
(
|
|
982
1161
|
SELECT COUNT(*)
|
|
983
1162
|
FROM examples e
|
|
@@ -1005,23 +1184,36 @@ function openStorage(options) {
|
|
|
1005
1184
|
consecutiveRetestFailures: row2.consecutive_retest_failures,
|
|
1006
1185
|
trained: row2.trained === 1,
|
|
1007
1186
|
modelPath: row2.model_path,
|
|
1008
|
-
needleVersion: row2.needle_version
|
|
1187
|
+
needleVersion: row2.needle_version,
|
|
1188
|
+
dataEpoch: row2.data_epoch,
|
|
1189
|
+
clearPending: row2.clear_pending === 1,
|
|
1190
|
+
clearErased: row2.clear_erased === 1,
|
|
1191
|
+
clearArtifactGenerationMax: row2.clear_artifact_generation_max,
|
|
1192
|
+
trainingLeaseOwner: row2.training_lease_owner,
|
|
1193
|
+
trainingLeaseEpoch: row2.training_lease_epoch,
|
|
1194
|
+
trainingLeaseUntil: row2.training_lease_until
|
|
1009
1195
|
};
|
|
1010
1196
|
},
|
|
1011
|
-
addExample(input, result) {
|
|
1197
|
+
addExample(input, result, expectedDataEpoch) {
|
|
1012
1198
|
assertOpen(closed);
|
|
1013
|
-
const generation = activeGeneration(database, options.name);
|
|
1014
1199
|
const split = assignExampleSplit(options.name, input);
|
|
1015
1200
|
const createdAt = Date.now();
|
|
1016
1201
|
const resultJson = stringifyJson(result, "classification result");
|
|
1017
1202
|
let id = 0;
|
|
1203
|
+
let generation = 0;
|
|
1204
|
+
let accepted = false;
|
|
1018
1205
|
transaction(database, () => {
|
|
1206
|
+
const state = classifierState(database, options.name);
|
|
1207
|
+
const epoch = expectedDataEpoch ?? state.data_epoch;
|
|
1208
|
+
if (state.data_epoch !== epoch || state.clear_pending === 1) return;
|
|
1209
|
+
generation = state.active_generation;
|
|
1019
1210
|
const insertion = database.prepare(`
|
|
1020
1211
|
INSERT INTO examples (
|
|
1021
1212
|
classifier_name, generation, input, result_json, split, created_at
|
|
1022
1213
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
1023
1214
|
`).run(options.name, generation, input, resultJson, split, createdAt);
|
|
1024
1215
|
id = Number(insertion.lastInsertRowid);
|
|
1216
|
+
accepted = true;
|
|
1025
1217
|
database.prepare(`
|
|
1026
1218
|
UPDATE classifiers
|
|
1027
1219
|
SET total_examples_logged = total_examples_logged + 1,
|
|
@@ -1031,7 +1223,7 @@ function openStorage(options) {
|
|
|
1031
1223
|
`).run(createdAt, options.name);
|
|
1032
1224
|
trimExamples(database, options.name, generation, options.maxTrainingSet);
|
|
1033
1225
|
});
|
|
1034
|
-
return { id, generation, input, result, split, createdAt };
|
|
1226
|
+
return accepted ? { id, generation, input, result, split, createdAt } : null;
|
|
1035
1227
|
},
|
|
1036
1228
|
listExamples(split, generation) {
|
|
1037
1229
|
assertOpen(closed);
|
|
@@ -1049,44 +1241,76 @@ function openStorage(options) {
|
|
|
1049
1241
|
`).all(options.name, selectedGeneration, split);
|
|
1050
1242
|
return rows.map((value) => mapExample(row(value)));
|
|
1051
1243
|
},
|
|
1052
|
-
|
|
1244
|
+
listExamplesForTraining(split, generation, expectedDataEpoch) {
|
|
1245
|
+
assertOpen(closed);
|
|
1246
|
+
let examples = null;
|
|
1247
|
+
transaction(database, () => {
|
|
1248
|
+
const state = classifierState(database, options.name);
|
|
1249
|
+
if (state.data_epoch !== expectedDataEpoch || state.clear_pending === 1 || state.active_generation !== generation) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
examples = database.prepare(`
|
|
1253
|
+
SELECT id, generation, input, result_json, split, created_at
|
|
1254
|
+
FROM examples
|
|
1255
|
+
WHERE classifier_name = ? AND generation = ? AND split = ?
|
|
1256
|
+
ORDER BY id
|
|
1257
|
+
`).all(options.name, generation, split).map(
|
|
1258
|
+
(value) => mapExample(row(value))
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
return examples;
|
|
1262
|
+
},
|
|
1263
|
+
markTrainingAttempted(exampleCount, expectedDataEpoch) {
|
|
1053
1264
|
assertOpen(closed);
|
|
1054
1265
|
if (!Number.isSafeInteger(exampleCount) || exampleCount <= 0) {
|
|
1055
1266
|
throw new TypeError("training example count must be a positive integer");
|
|
1056
1267
|
}
|
|
1057
|
-
|
|
1268
|
+
const epoch = expectedDataEpoch ?? storage.snapshot().dataEpoch;
|
|
1269
|
+
const result = database.prepare(`
|
|
1058
1270
|
UPDATE classifiers
|
|
1059
1271
|
SET new_examples_since_training = 0,
|
|
1060
1272
|
training_attempts = training_attempts + 1,
|
|
1061
1273
|
examples_used_for_training = ?,
|
|
1062
1274
|
updated_at = ?
|
|
1063
|
-
WHERE name = ?
|
|
1064
|
-
`).run(exampleCount, Date.now(), options.name);
|
|
1275
|
+
WHERE name = ? AND data_epoch = ? AND clear_pending = 0
|
|
1276
|
+
`).run(exampleCount, Date.now(), options.name, epoch);
|
|
1277
|
+
return result.changes === 1;
|
|
1065
1278
|
},
|
|
1066
|
-
promoteGeneration(model) {
|
|
1279
|
+
promoteGeneration(model, expectedDataEpoch) {
|
|
1067
1280
|
assertOpen(closed);
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1281
|
+
let generation = 0;
|
|
1282
|
+
let promoted = false;
|
|
1283
|
+
transaction(database, () => {
|
|
1284
|
+
const state = classifierState(database, options.name);
|
|
1285
|
+
const epoch = expectedDataEpoch ?? state.data_epoch;
|
|
1286
|
+
if (state.data_epoch !== epoch || state.clear_pending === 1) return;
|
|
1287
|
+
generation = state.active_generation;
|
|
1288
|
+
database.prepare(`
|
|
1289
|
+
UPDATE generations
|
|
1290
|
+
SET trained = 1, model_path = ?, needle_version = ?
|
|
1291
|
+
WHERE classifier_name = ? AND generation = ?
|
|
1292
|
+
`).run(model.modelPath, model.needleVersion, options.name, generation);
|
|
1293
|
+
promoted = true;
|
|
1294
|
+
});
|
|
1295
|
+
return promoted ? generationByNumber(database, options.name, generation) : null;
|
|
1075
1296
|
},
|
|
1076
|
-
recordLocalClassification() {
|
|
1297
|
+
recordLocalClassification(expectedDataEpoch) {
|
|
1077
1298
|
assertOpen(closed);
|
|
1078
|
-
|
|
1299
|
+
const epoch = expectedDataEpoch ?? storage.snapshot().dataEpoch;
|
|
1300
|
+
const result = database.prepare(`
|
|
1079
1301
|
UPDATE classifiers
|
|
1080
1302
|
SET local_classifications_since_retest = local_classifications_since_retest + 1,
|
|
1081
1303
|
total_local_classifications = total_local_classifications + 1,
|
|
1082
1304
|
updated_at = ?
|
|
1083
|
-
WHERE name = ?
|
|
1084
|
-
`).run(Date.now(), options.name);
|
|
1305
|
+
WHERE name = ? AND data_epoch = ? AND clear_pending = 0
|
|
1306
|
+
`).run(Date.now(), options.name, epoch);
|
|
1307
|
+
if (result.changes !== 1) return null;
|
|
1085
1308
|
return storage.snapshot().localClassificationsSinceRetest;
|
|
1086
1309
|
},
|
|
1087
|
-
recordRetest(passed) {
|
|
1310
|
+
recordRetest(passed, expectedDataEpoch) {
|
|
1088
1311
|
assertOpen(closed);
|
|
1089
|
-
|
|
1312
|
+
const epoch = expectedDataEpoch ?? storage.snapshot().dataEpoch;
|
|
1313
|
+
const result = database.prepare(`
|
|
1090
1314
|
UPDATE classifiers
|
|
1091
1315
|
SET local_classifications_since_retest = 0,
|
|
1092
1316
|
total_retests = total_retests + 1,
|
|
@@ -1095,28 +1319,33 @@ function openStorage(options) {
|
|
|
1095
1319
|
ELSE consecutive_retest_failures + 1
|
|
1096
1320
|
END,
|
|
1097
1321
|
updated_at = ?
|
|
1098
|
-
WHERE name = ?
|
|
1099
|
-
`).run(passed ? 1 : 0, Date.now(), options.name);
|
|
1322
|
+
WHERE name = ? AND data_epoch = ? AND clear_pending = 0
|
|
1323
|
+
`).run(passed ? 1 : 0, Date.now(), options.name, epoch);
|
|
1324
|
+
if (result.changes !== 1) return null;
|
|
1100
1325
|
return storage.snapshot().consecutiveRetestFailures;
|
|
1101
1326
|
},
|
|
1102
|
-
claimTrainingLease(owner, durationMs) {
|
|
1327
|
+
claimTrainingLease(owner, durationMs, expectedDataEpoch) {
|
|
1103
1328
|
assertOpen(closed);
|
|
1104
1329
|
if (owner.trim() === "" || !Number.isSafeInteger(durationMs) || durationMs <= 0) {
|
|
1105
1330
|
throw new TypeError("training lease needs an owner and positive duration");
|
|
1106
1331
|
}
|
|
1107
|
-
const
|
|
1332
|
+
const now = Date.now();
|
|
1333
|
+
const epoch = expectedDataEpoch ?? storage.snapshot().dataEpoch;
|
|
1108
1334
|
const result = database.prepare(`
|
|
1109
1335
|
UPDATE classifiers
|
|
1110
1336
|
SET training_lease_owner = ?,
|
|
1111
1337
|
training_lease_until = ?,
|
|
1338
|
+
training_lease_epoch = ?,
|
|
1112
1339
|
updated_at = ?
|
|
1113
1340
|
WHERE name = ?
|
|
1341
|
+
AND data_epoch = ?
|
|
1342
|
+
AND clear_pending = 0
|
|
1114
1343
|
AND (
|
|
1115
1344
|
training_lease_owner IS NULL
|
|
1116
1345
|
OR training_lease_until <= ?
|
|
1117
1346
|
OR training_lease_owner = ?
|
|
1118
1347
|
)
|
|
1119
|
-
`).run(owner,
|
|
1348
|
+
`).run(owner, now + durationMs, epoch, now, options.name, epoch, now, owner);
|
|
1120
1349
|
return result.changes === 1;
|
|
1121
1350
|
},
|
|
1122
1351
|
releaseTrainingLease(owner) {
|
|
@@ -1125,16 +1354,21 @@ function openStorage(options) {
|
|
|
1125
1354
|
UPDATE classifiers
|
|
1126
1355
|
SET training_lease_owner = NULL,
|
|
1127
1356
|
training_lease_until = NULL,
|
|
1357
|
+
training_lease_epoch = NULL,
|
|
1128
1358
|
updated_at = ?
|
|
1129
1359
|
WHERE name = ? AND training_lease_owner = ?
|
|
1130
1360
|
`).run(Date.now(), options.name, owner);
|
|
1131
1361
|
},
|
|
1132
|
-
archiveAndReset() {
|
|
1362
|
+
archiveAndReset(expectedDataEpoch) {
|
|
1133
1363
|
assertOpen(closed);
|
|
1134
1364
|
let nextGeneration = 0;
|
|
1135
1365
|
const changedAt = Date.now();
|
|
1366
|
+
let reset = false;
|
|
1136
1367
|
transaction(database, () => {
|
|
1137
|
-
const
|
|
1368
|
+
const state = classifierState(database, options.name);
|
|
1369
|
+
const epoch = expectedDataEpoch ?? state.data_epoch;
|
|
1370
|
+
if (state.data_epoch !== epoch || state.clear_pending === 1) return;
|
|
1371
|
+
const currentGeneration = state.active_generation;
|
|
1138
1372
|
database.prepare(`
|
|
1139
1373
|
UPDATE generations
|
|
1140
1374
|
SET status = 'archived', archived_at = ?
|
|
@@ -1154,19 +1388,141 @@ function openStorage(options) {
|
|
|
1154
1388
|
database.prepare(`
|
|
1155
1389
|
UPDATE classifiers
|
|
1156
1390
|
SET active_generation = ?,
|
|
1391
|
+
data_epoch = data_epoch + 1,
|
|
1157
1392
|
new_examples_since_training = 0,
|
|
1158
1393
|
training_attempts = 0,
|
|
1159
1394
|
examples_used_for_training = 0,
|
|
1160
1395
|
training_lease_owner = NULL,
|
|
1161
1396
|
training_lease_until = NULL,
|
|
1397
|
+
training_lease_epoch = NULL,
|
|
1398
|
+
local_classifications_since_retest = 0,
|
|
1399
|
+
consecutive_retest_failures = 0,
|
|
1400
|
+
updated_at = ?
|
|
1401
|
+
WHERE name = ?
|
|
1402
|
+
`).run(nextGeneration, changedAt, options.name);
|
|
1403
|
+
reset = true;
|
|
1404
|
+
});
|
|
1405
|
+
return reset ? generationByNumber(database, options.name, nextGeneration) : null;
|
|
1406
|
+
},
|
|
1407
|
+
beginClearTrainingData() {
|
|
1408
|
+
assertOpen(closed);
|
|
1409
|
+
database.prepare(`
|
|
1410
|
+
UPDATE classifiers
|
|
1411
|
+
SET data_epoch = data_epoch + 1,
|
|
1412
|
+
clear_pending = 1,
|
|
1413
|
+
clear_erased = 0,
|
|
1414
|
+
clear_artifact_generation_max = (
|
|
1415
|
+
SELECT MAX(generation)
|
|
1416
|
+
FROM generations
|
|
1417
|
+
WHERE classifier_name = ?
|
|
1418
|
+
),
|
|
1419
|
+
updated_at = ?
|
|
1420
|
+
WHERE name = ?
|
|
1421
|
+
`).run(options.name, Date.now(), options.name);
|
|
1422
|
+
return classifierState(database, options.name).data_epoch;
|
|
1423
|
+
},
|
|
1424
|
+
eraseTrainingData(dataEpoch) {
|
|
1425
|
+
assertOpen(closed);
|
|
1426
|
+
let nextGeneration = 0;
|
|
1427
|
+
const changedAt = Date.now();
|
|
1428
|
+
let erased = false;
|
|
1429
|
+
transaction(database, () => {
|
|
1430
|
+
const state = classifierState(database, options.name);
|
|
1431
|
+
if (state.data_epoch !== dataEpoch || state.clear_pending !== 1) return;
|
|
1432
|
+
if (state.clear_erased === 1) return;
|
|
1433
|
+
const maximum = requiredRow(database.prepare(`
|
|
1434
|
+
SELECT MAX(generation) AS maximum
|
|
1435
|
+
FROM generations
|
|
1436
|
+
WHERE classifier_name = ?
|
|
1437
|
+
`).get(options.name));
|
|
1438
|
+
nextGeneration = maximum.maximum + 1;
|
|
1439
|
+
database.prepare(`
|
|
1440
|
+
DELETE FROM examples WHERE classifier_name = ?
|
|
1441
|
+
`).run(options.name);
|
|
1442
|
+
database.prepare(`
|
|
1443
|
+
DELETE FROM generations WHERE classifier_name = ?
|
|
1444
|
+
`).run(options.name);
|
|
1445
|
+
database.prepare(`
|
|
1446
|
+
INSERT INTO generations (
|
|
1447
|
+
classifier_name, generation, status, created_at
|
|
1448
|
+
) VALUES (?, ?, 'active', ?)
|
|
1449
|
+
`).run(options.name, nextGeneration, changedAt);
|
|
1450
|
+
database.prepare(`
|
|
1451
|
+
UPDATE classifiers
|
|
1452
|
+
SET active_generation = ?,
|
|
1453
|
+
total_examples_logged = 0,
|
|
1454
|
+
new_examples_since_training = 0,
|
|
1455
|
+
training_attempts = 0,
|
|
1456
|
+
examples_used_for_training = 0,
|
|
1162
1457
|
local_classifications_since_retest = 0,
|
|
1458
|
+
total_local_classifications = 0,
|
|
1459
|
+
total_retests = 0,
|
|
1163
1460
|
consecutive_retest_failures = 0,
|
|
1461
|
+
training_lease_owner = NULL,
|
|
1462
|
+
training_lease_until = NULL,
|
|
1463
|
+
training_lease_epoch = NULL,
|
|
1464
|
+
clear_erased = 1,
|
|
1164
1465
|
updated_at = ?
|
|
1165
1466
|
WHERE name = ?
|
|
1166
1467
|
`).run(nextGeneration, changedAt, options.name);
|
|
1468
|
+
erased = true;
|
|
1167
1469
|
});
|
|
1470
|
+
if (!erased) return null;
|
|
1471
|
+
truncateWriteAheadLog(database);
|
|
1168
1472
|
return generationByNumber(database, options.name, nextGeneration);
|
|
1169
1473
|
},
|
|
1474
|
+
finishClearTrainingData(dataEpoch) {
|
|
1475
|
+
assertOpen(closed);
|
|
1476
|
+
const state = classifierState(database, options.name);
|
|
1477
|
+
if (state.data_epoch !== dataEpoch || state.clear_pending !== 1 || state.clear_erased !== 1) {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
truncateWriteAheadLog(database);
|
|
1481
|
+
const result = database.prepare(`
|
|
1482
|
+
UPDATE classifiers
|
|
1483
|
+
SET clear_pending = 0, updated_at = ?
|
|
1484
|
+
WHERE name = ?
|
|
1485
|
+
AND data_epoch = ?
|
|
1486
|
+
AND clear_pending = 1
|
|
1487
|
+
AND clear_erased = 1
|
|
1488
|
+
`).run(Date.now(), options.name, dataEpoch);
|
|
1489
|
+
return result.changes === 1;
|
|
1490
|
+
},
|
|
1491
|
+
clearTrainingData() {
|
|
1492
|
+
const dataEpoch = storage.beginClearTrainingData();
|
|
1493
|
+
if (!storage.claimArtifactWriteLock()) {
|
|
1494
|
+
throw new Error("Classifier artifacts are currently being written");
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const generation = storage.eraseTrainingData(dataEpoch);
|
|
1498
|
+
if (generation === null || !storage.finishClearTrainingData(dataEpoch)) {
|
|
1499
|
+
throw new Error("Training data clear was superseded");
|
|
1500
|
+
}
|
|
1501
|
+
return generation;
|
|
1502
|
+
} finally {
|
|
1503
|
+
storage.releaseArtifactWriteLock();
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
claimArtifactWriteLock() {
|
|
1507
|
+
assertOpen(closed);
|
|
1508
|
+
if (artifactLockHeld) return true;
|
|
1509
|
+
try {
|
|
1510
|
+
initializedArtifactLockDatabase.exec("BEGIN IMMEDIATE");
|
|
1511
|
+
artifactLockHeld = true;
|
|
1512
|
+
return true;
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ERR_SQLITE_ERROR" && "message" in error && typeof error.message === "string" && /locked|busy/i.test(error.message)) {
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
throw error;
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
releaseArtifactWriteLock() {
|
|
1521
|
+
assertOpen(closed);
|
|
1522
|
+
if (!artifactLockHeld) return;
|
|
1523
|
+
initializedArtifactLockDatabase.exec("COMMIT");
|
|
1524
|
+
artifactLockHeld = false;
|
|
1525
|
+
},
|
|
1170
1526
|
listGenerations() {
|
|
1171
1527
|
assertOpen(closed);
|
|
1172
1528
|
return database.prepare(`
|
|
@@ -1182,6 +1538,11 @@ function openStorage(options) {
|
|
|
1182
1538
|
if (closed) {
|
|
1183
1539
|
return;
|
|
1184
1540
|
}
|
|
1541
|
+
if (artifactLockHeld) {
|
|
1542
|
+
initializedArtifactLockDatabase.exec("ROLLBACK");
|
|
1543
|
+
artifactLockHeld = false;
|
|
1544
|
+
}
|
|
1545
|
+
initializedArtifactLockDatabase.close();
|
|
1185
1546
|
closed = true;
|
|
1186
1547
|
database.close();
|
|
1187
1548
|
}
|
|
@@ -1194,6 +1555,69 @@ function activeGeneration(database, name) {
|
|
|
1194
1555
|
`).get(name));
|
|
1195
1556
|
return active.active_generation;
|
|
1196
1557
|
}
|
|
1558
|
+
function truncateWriteAheadLog(database) {
|
|
1559
|
+
database.exec("PRAGMA busy_timeout = 0");
|
|
1560
|
+
let checkpoint;
|
|
1561
|
+
try {
|
|
1562
|
+
checkpoint = requiredRow(database.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get());
|
|
1563
|
+
} finally {
|
|
1564
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1565
|
+
}
|
|
1566
|
+
if (checkpoint.busy !== 0) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
"SQLite WAL could not be truncated after clearing training data"
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function classifierState(database, name) {
|
|
1573
|
+
return requiredRow(database.prepare(`
|
|
1574
|
+
SELECT
|
|
1575
|
+
active_generation,
|
|
1576
|
+
data_epoch,
|
|
1577
|
+
clear_pending,
|
|
1578
|
+
clear_erased,
|
|
1579
|
+
clear_artifact_generation_max,
|
|
1580
|
+
training_lease_owner,
|
|
1581
|
+
training_lease_epoch,
|
|
1582
|
+
training_lease_until
|
|
1583
|
+
FROM classifiers
|
|
1584
|
+
WHERE name = ?
|
|
1585
|
+
`).get(name));
|
|
1586
|
+
}
|
|
1587
|
+
function migrateClassifierColumns(database) {
|
|
1588
|
+
transaction(database, () => {
|
|
1589
|
+
const columns = new Set(
|
|
1590
|
+
database.prepare("PRAGMA table_info(classifiers)").all().map(
|
|
1591
|
+
(value) => row(value).name
|
|
1592
|
+
)
|
|
1593
|
+
);
|
|
1594
|
+
if (!columns.has("data_epoch")) {
|
|
1595
|
+
database.exec(
|
|
1596
|
+
"ALTER TABLE classifiers ADD COLUMN data_epoch INTEGER NOT NULL DEFAULT 0"
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
if (!columns.has("clear_pending")) {
|
|
1600
|
+
database.exec(
|
|
1601
|
+
"ALTER TABLE classifiers ADD COLUMN clear_pending INTEGER NOT NULL DEFAULT 0"
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (!columns.has("clear_erased")) {
|
|
1605
|
+
database.exec(
|
|
1606
|
+
"ALTER TABLE classifiers ADD COLUMN clear_erased INTEGER NOT NULL DEFAULT 0"
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
if (!columns.has("clear_artifact_generation_max")) {
|
|
1610
|
+
database.exec(
|
|
1611
|
+
"ALTER TABLE classifiers ADD COLUMN clear_artifact_generation_max INTEGER"
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
if (!columns.has("training_lease_epoch")) {
|
|
1615
|
+
database.exec(
|
|
1616
|
+
"ALTER TABLE classifiers ADD COLUMN training_lease_epoch INTEGER"
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1197
1621
|
function generationByNumber(database, name, generation) {
|
|
1198
1622
|
const stored = requiredRow(database.prepare(`
|
|
1199
1623
|
SELECT
|
|
@@ -1325,6 +1749,7 @@ function requiredRow(value) {
|
|
|
1325
1749
|
// src/classifier.ts
|
|
1326
1750
|
var TRAINING_LEASE_DURATION_MS = 5 * 60 * 1e3;
|
|
1327
1751
|
var TRAINING_LEASE_RENEWAL_MS = 60 * 1e3;
|
|
1752
|
+
var ARTIFACT_LOCK_WAIT_MS = 3e4;
|
|
1328
1753
|
function init(inputConfig) {
|
|
1329
1754
|
const config = normalizeConfig(inputConfig);
|
|
1330
1755
|
const dataDirectory = config.dataDirectory ?? join3(process.cwd(), ".swapai");
|
|
@@ -1362,30 +1787,65 @@ function openClassifierStorage(config, dataDirectory) {
|
|
|
1362
1787
|
}
|
|
1363
1788
|
}
|
|
1364
1789
|
function createClassifier(config, storage, runtime) {
|
|
1790
|
+
const initialState = storage.snapshot();
|
|
1365
1791
|
let closed = false;
|
|
1366
|
-
let
|
|
1792
|
+
let closing = false;
|
|
1793
|
+
let closePromise = null;
|
|
1794
|
+
let trained = initialState.trained && !initialState.clearPending;
|
|
1367
1795
|
let loadedModel = null;
|
|
1796
|
+
let loadedModelEpoch = null;
|
|
1368
1797
|
let operationQueue = Promise.resolve();
|
|
1369
1798
|
let trainingQueue = Promise.resolve();
|
|
1370
1799
|
let trainingScheduled = false;
|
|
1371
1800
|
const trainingLeaseOwner = `${process.pid}:${randomUUID2()}`;
|
|
1372
1801
|
let classificationQueue = Promise.resolve();
|
|
1373
1802
|
let queuedFailure = null;
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1803
|
+
let clearFailure = null;
|
|
1804
|
+
let dataEpoch = initialState.dataEpoch;
|
|
1805
|
+
let clearedEpoch = initialState.clearPending ? initialState.dataEpoch - 1 : initialState.dataEpoch;
|
|
1806
|
+
const saved = initialState;
|
|
1807
|
+
if (!saved.clearPending && saved.trained && (saved.modelPath === null || saved.needleVersion !== NEEDLE_VERSION)) {
|
|
1808
|
+
if (storage.archiveAndReset(saved.dataEpoch) !== null) {
|
|
1809
|
+
const reset = storage.snapshot();
|
|
1810
|
+
dataEpoch = reset.dataEpoch;
|
|
1811
|
+
clearedEpoch = reset.dataEpoch;
|
|
1812
|
+
trained = false;
|
|
1813
|
+
}
|
|
1378
1814
|
}
|
|
1379
1815
|
const startup = startRuntime();
|
|
1380
1816
|
async function startRuntime() {
|
|
1817
|
+
const startupEpoch = dataEpoch;
|
|
1818
|
+
const beforeRuntime = storage.snapshot();
|
|
1819
|
+
if (beforeRuntime.clearPending) {
|
|
1820
|
+
trained = false;
|
|
1821
|
+
dataEpoch = beforeRuntime.dataEpoch;
|
|
1822
|
+
try {
|
|
1823
|
+
await performDurableClear(beforeRuntime.dataEpoch);
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
rememberClearFailure(error);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1381
1828
|
try {
|
|
1382
1829
|
await runtime.ready();
|
|
1383
1830
|
const snapshot = storage.snapshot();
|
|
1831
|
+
if (snapshot.clearPending) {
|
|
1832
|
+
trained = false;
|
|
1833
|
+
dataEpoch = snapshot.dataEpoch;
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1384
1836
|
if (snapshot.trained && snapshot.modelPath !== null) {
|
|
1385
|
-
|
|
1837
|
+
const restoredModel = await runtime.loadModel({
|
|
1386
1838
|
modelPath: snapshot.modelPath,
|
|
1387
1839
|
resultConfig: config.result
|
|
1388
1840
|
});
|
|
1841
|
+
const current = storage.snapshot();
|
|
1842
|
+
if (startupEpoch === current.dataEpoch && !current.clearPending && current.trained) {
|
|
1843
|
+
loadedModel = restoredModel;
|
|
1844
|
+
loadedModelEpoch = startupEpoch;
|
|
1845
|
+
trained = true;
|
|
1846
|
+
} else {
|
|
1847
|
+
await restoredModel.close();
|
|
1848
|
+
}
|
|
1389
1849
|
}
|
|
1390
1850
|
} catch (error) {
|
|
1391
1851
|
const swapAIError = toSwapAIError(
|
|
@@ -1400,6 +1860,25 @@ function createClassifier(config, storage, runtime) {
|
|
|
1400
1860
|
queuedFailure = error;
|
|
1401
1861
|
reportBackgroundError(config, error);
|
|
1402
1862
|
}
|
|
1863
|
+
function rememberClearFailure(error) {
|
|
1864
|
+
clearFailure = toSwapAIError(
|
|
1865
|
+
error,
|
|
1866
|
+
"storage_failed",
|
|
1867
|
+
`Could not completely clear classifier "${config.name}"`
|
|
1868
|
+
);
|
|
1869
|
+
reportBackgroundError(config, clearFailure);
|
|
1870
|
+
}
|
|
1871
|
+
function refreshStoredState() {
|
|
1872
|
+
const snapshot = storage.snapshot();
|
|
1873
|
+
dataEpoch = snapshot.dataEpoch;
|
|
1874
|
+
if (snapshot.clearPending) {
|
|
1875
|
+
trained = false;
|
|
1876
|
+
} else {
|
|
1877
|
+
trained = snapshot.trained;
|
|
1878
|
+
clearedEpoch = snapshot.dataEpoch;
|
|
1879
|
+
}
|
|
1880
|
+
return snapshot;
|
|
1881
|
+
}
|
|
1403
1882
|
function enqueue(operation) {
|
|
1404
1883
|
const result = operationQueue.then(operation);
|
|
1405
1884
|
operationQueue = result.catch((error) => {
|
|
@@ -1409,14 +1888,15 @@ function createClassifier(config, storage, runtime) {
|
|
|
1409
1888
|
});
|
|
1410
1889
|
return result;
|
|
1411
1890
|
}
|
|
1412
|
-
function persistExample(input, result) {
|
|
1891
|
+
function persistExample(input, result, epoch = dataEpoch) {
|
|
1892
|
+
if (epoch !== dataEpoch) return Promise.resolve();
|
|
1413
1893
|
return enqueue(() => {
|
|
1414
|
-
|
|
1415
|
-
scheduleTraining();
|
|
1894
|
+
if (epoch !== dataEpoch) return;
|
|
1895
|
+
if (storage.addExample(input, result, epoch) !== null) scheduleTraining();
|
|
1416
1896
|
});
|
|
1417
1897
|
}
|
|
1418
1898
|
function shouldTrain(snapshot = storage.snapshot()) {
|
|
1419
|
-
return !snapshot.trained && snapshot.examplesUsedForTraining < config.maxTrainingSet && snapshot.newExamplesSinceTraining >= config.retrainOnCount;
|
|
1899
|
+
return clearedEpoch === dataEpoch && snapshot.dataEpoch === dataEpoch && !snapshot.clearPending && !snapshot.trained && snapshot.examplesUsedForTraining < config.maxTrainingSet && snapshot.newExamplesSinceTraining >= config.retrainOnCount;
|
|
1420
1900
|
}
|
|
1421
1901
|
function scheduleTraining() {
|
|
1422
1902
|
if (trainingScheduled || !shouldTrain()) return;
|
|
@@ -1435,8 +1915,13 @@ function createClassifier(config, storage, runtime) {
|
|
|
1435
1915
|
});
|
|
1436
1916
|
}
|
|
1437
1917
|
async function trainWhenDue() {
|
|
1918
|
+
const trainingEpoch = dataEpoch;
|
|
1438
1919
|
if (!shouldTrain()) return false;
|
|
1439
|
-
if (!storage.claimTrainingLease(
|
|
1920
|
+
if (!storage.claimTrainingLease(
|
|
1921
|
+
trainingLeaseOwner,
|
|
1922
|
+
TRAINING_LEASE_DURATION_MS,
|
|
1923
|
+
trainingEpoch
|
|
1924
|
+
)) {
|
|
1440
1925
|
return false;
|
|
1441
1926
|
}
|
|
1442
1927
|
let leaseHeld = true;
|
|
@@ -1445,7 +1930,8 @@ function createClassifier(config, storage, runtime) {
|
|
|
1445
1930
|
try {
|
|
1446
1931
|
leaseHeld = storage.claimTrainingLease(
|
|
1447
1932
|
trainingLeaseOwner,
|
|
1448
|
-
TRAINING_LEASE_DURATION_MS
|
|
1933
|
+
TRAINING_LEASE_DURATION_MS,
|
|
1934
|
+
trainingEpoch
|
|
1449
1935
|
);
|
|
1450
1936
|
} catch {
|
|
1451
1937
|
leaseHeld = false;
|
|
@@ -1460,15 +1946,30 @@ function createClassifier(config, storage, runtime) {
|
|
|
1460
1946
|
try {
|
|
1461
1947
|
const snapshot = storage.snapshot();
|
|
1462
1948
|
if (!shouldTrain(snapshot)) return false;
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1949
|
+
if (!storage.markTrainingAttempted(
|
|
1950
|
+
snapshot.activeExampleCount,
|
|
1951
|
+
trainingEpoch
|
|
1952
|
+
)) {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
const trainingExamples = storage.listExamplesForTraining(
|
|
1956
|
+
"training",
|
|
1957
|
+
snapshot.activeGeneration,
|
|
1958
|
+
trainingEpoch
|
|
1959
|
+
);
|
|
1960
|
+
const heldOutExamples = storage.listExamplesForTraining(
|
|
1961
|
+
"held_out",
|
|
1962
|
+
snapshot.activeGeneration,
|
|
1963
|
+
trainingEpoch
|
|
1964
|
+
);
|
|
1965
|
+
if (trainingExamples === null || heldOutExamples === null) return false;
|
|
1466
1966
|
if (trainingExamples.length === 0 || heldOutExamples.length === 0) {
|
|
1467
1967
|
return true;
|
|
1468
1968
|
}
|
|
1469
1969
|
const candidate = await runtime.train({
|
|
1470
1970
|
classifierName: config.name,
|
|
1471
1971
|
generation: snapshot.activeGeneration,
|
|
1972
|
+
expectedEpoch: trainingEpoch,
|
|
1472
1973
|
examples: trainingExamples.map(({ input, result }) => ({ input, result })),
|
|
1473
1974
|
resultConfig: config.result
|
|
1474
1975
|
});
|
|
@@ -1492,14 +1993,43 @@ function createClassifier(config, storage, runtime) {
|
|
|
1492
1993
|
return true;
|
|
1493
1994
|
}
|
|
1494
1995
|
if (!renewTrainingLease()) {
|
|
1996
|
+
const current = refreshStoredState();
|
|
1997
|
+
if (current.dataEpoch !== trainingEpoch || current.clearPending) {
|
|
1998
|
+
await runtime.clearClassifierGenerationArtifacts(
|
|
1999
|
+
config.name,
|
|
2000
|
+
snapshot.activeGeneration
|
|
2001
|
+
);
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
1495
2004
|
throw new SwapAIError(
|
|
1496
2005
|
"service_unavailable",
|
|
1497
2006
|
`Classifier "${config.name}" lost its training lease`
|
|
1498
2007
|
);
|
|
1499
2008
|
}
|
|
1500
|
-
|
|
2009
|
+
if (trainingEpoch !== refreshStoredState().dataEpoch) {
|
|
2010
|
+
await runtime.clearClassifierGenerationArtifacts(
|
|
2011
|
+
config.name,
|
|
2012
|
+
snapshot.activeGeneration
|
|
2013
|
+
);
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
if (storage.promoteGeneration(candidate, trainingEpoch) === null) {
|
|
2017
|
+
await runtime.clearClassifierGenerationArtifacts(
|
|
2018
|
+
config.name,
|
|
2019
|
+
snapshot.activeGeneration
|
|
2020
|
+
);
|
|
2021
|
+
return false;
|
|
2022
|
+
}
|
|
1501
2023
|
if (loadedModel !== null) await loadedModel.close();
|
|
2024
|
+
if (trainingEpoch !== refreshStoredState().dataEpoch) {
|
|
2025
|
+
await runtime.clearClassifierGenerationArtifacts(
|
|
2026
|
+
config.name,
|
|
2027
|
+
snapshot.activeGeneration
|
|
2028
|
+
);
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
1502
2031
|
loadedModel = candidateModel;
|
|
2032
|
+
loadedModelEpoch = trainingEpoch;
|
|
1503
2033
|
trained = true;
|
|
1504
2034
|
return true;
|
|
1505
2035
|
} finally {
|
|
@@ -1510,34 +2040,49 @@ function createClassifier(config, storage, runtime) {
|
|
|
1510
2040
|
storage.releaseTrainingLease(trainingLeaseOwner);
|
|
1511
2041
|
}
|
|
1512
2042
|
}
|
|
1513
|
-
async function model() {
|
|
2043
|
+
async function model(expectedEpoch) {
|
|
1514
2044
|
await startup;
|
|
1515
|
-
|
|
2045
|
+
const current = refreshStoredState();
|
|
2046
|
+
if (!trained || current.clearPending || current.dataEpoch !== expectedEpoch) {
|
|
1516
2047
|
throw new SwapAIError(
|
|
1517
2048
|
"not_trained",
|
|
1518
2049
|
`Classifier "${config.name}" has not passed its held-out test`
|
|
1519
2050
|
);
|
|
1520
2051
|
}
|
|
2052
|
+
if (loadedModel !== null && loadedModelEpoch !== expectedEpoch) {
|
|
2053
|
+
await loadedModel.close();
|
|
2054
|
+
loadedModel = null;
|
|
2055
|
+
loadedModelEpoch = null;
|
|
2056
|
+
}
|
|
1521
2057
|
if (loadedModel === null) {
|
|
1522
|
-
|
|
1523
|
-
if (snapshot.modelPath === null) {
|
|
2058
|
+
if (current.modelPath === null) {
|
|
1524
2059
|
throw new SwapAIError(
|
|
1525
2060
|
"service_unavailable",
|
|
1526
2061
|
`Classifier "${config.name}" is trained but has no saved model`
|
|
1527
2062
|
);
|
|
1528
2063
|
}
|
|
1529
2064
|
await runtime.ready();
|
|
1530
|
-
|
|
1531
|
-
modelPath:
|
|
2065
|
+
const restored = await runtime.loadModel({
|
|
2066
|
+
modelPath: current.modelPath,
|
|
1532
2067
|
resultConfig: config.result
|
|
1533
2068
|
});
|
|
2069
|
+
const afterLoad = refreshStoredState();
|
|
2070
|
+
if (afterLoad.dataEpoch !== expectedEpoch || afterLoad.clearPending || !afterLoad.trained) {
|
|
2071
|
+
await restored.close();
|
|
2072
|
+
throw new SwapAIError(
|
|
2073
|
+
"not_trained",
|
|
2074
|
+
`Classifier "${config.name}" has been cleared`
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
loadedModel = restored;
|
|
2078
|
+
loadedModelEpoch = expectedEpoch;
|
|
1534
2079
|
}
|
|
1535
2080
|
return loadedModel;
|
|
1536
2081
|
}
|
|
1537
2082
|
async function callReference(input, reference) {
|
|
1538
2083
|
return validateResult(config.result, await reference(input));
|
|
1539
2084
|
}
|
|
1540
|
-
async function fallbackToReference(input, reference, candidateError, retestDue) {
|
|
2085
|
+
async function fallbackToReference(input, reference, candidateError, retestDue, epoch) {
|
|
1541
2086
|
if (reference === void 0) {
|
|
1542
2087
|
throw toSwapAIError(
|
|
1543
2088
|
candidateError,
|
|
@@ -1554,22 +2099,26 @@ function createClassifier(config, storage, runtime) {
|
|
|
1554
2099
|
)
|
|
1555
2100
|
);
|
|
1556
2101
|
const referenceResult = await callReference(input, reference);
|
|
1557
|
-
await persistExample(input, referenceResult);
|
|
1558
|
-
if (retestDue) {
|
|
1559
|
-
storage.recordLocalClassification();
|
|
1560
|
-
const failures = storage.recordRetest(false);
|
|
1561
|
-
if (failures >= config.retestRevertOn)
|
|
2102
|
+
await persistExample(input, referenceResult, epoch);
|
|
2103
|
+
if (retestDue && epoch === dataEpoch) {
|
|
2104
|
+
if (storage.recordLocalClassification(epoch) === null) return referenceResult;
|
|
2105
|
+
const failures = storage.recordRetest(false, epoch);
|
|
2106
|
+
if (failures !== null && failures >= config.retestRevertOn) {
|
|
2107
|
+
await disableModel(epoch);
|
|
2108
|
+
}
|
|
1562
2109
|
}
|
|
1563
2110
|
return referenceResult;
|
|
1564
2111
|
}
|
|
1565
|
-
async function disableModel() {
|
|
1566
|
-
storage.archiveAndReset();
|
|
2112
|
+
async function disableModel(epoch) {
|
|
2113
|
+
if (storage.archiveAndReset(epoch) === null) return;
|
|
2114
|
+
refreshStoredState();
|
|
1567
2115
|
trained = false;
|
|
1568
2116
|
const previousModel = loadedModel;
|
|
1569
2117
|
loadedModel = null;
|
|
2118
|
+
loadedModelEpoch = null;
|
|
1570
2119
|
if (previousModel !== null) await previousModel.close();
|
|
1571
2120
|
}
|
|
1572
|
-
async function classifyNow(input, reference) {
|
|
2121
|
+
async function classifyNow(input, reference, epoch) {
|
|
1573
2122
|
assertOpen2(closed);
|
|
1574
2123
|
if (!trained) {
|
|
1575
2124
|
if (reference === void 0) {
|
|
@@ -1579,7 +2128,7 @@ function createClassifier(config, storage, runtime) {
|
|
|
1579
2128
|
);
|
|
1580
2129
|
}
|
|
1581
2130
|
const referenceResult2 = await callReference(input, reference);
|
|
1582
|
-
await persistExample(input, referenceResult2);
|
|
2131
|
+
await persistExample(input, referenceResult2, epoch);
|
|
1583
2132
|
return referenceResult2;
|
|
1584
2133
|
}
|
|
1585
2134
|
const snapshot = storage.snapshot();
|
|
@@ -1588,37 +2137,190 @@ function createClassifier(config, storage, runtime) {
|
|
|
1588
2137
|
try {
|
|
1589
2138
|
candidateResult = validateResult(
|
|
1590
2139
|
config.result,
|
|
1591
|
-
await (await model()).classify(input)
|
|
2140
|
+
await (await model(epoch)).classify(input)
|
|
1592
2141
|
);
|
|
1593
2142
|
} catch (error) {
|
|
1594
|
-
return fallbackToReference(input, reference, error, retestDue);
|
|
2143
|
+
return fallbackToReference(input, reference, error, retestDue, epoch);
|
|
2144
|
+
}
|
|
2145
|
+
const afterClassification = refreshStoredState();
|
|
2146
|
+
if (afterClassification.dataEpoch !== epoch || afterClassification.clearPending) {
|
|
2147
|
+
return fallbackToReference(
|
|
2148
|
+
input,
|
|
2149
|
+
reference,
|
|
2150
|
+
new SwapAIError(
|
|
2151
|
+
"not_trained",
|
|
2152
|
+
`Classifier "${config.name}" was cleared during classification`
|
|
2153
|
+
),
|
|
2154
|
+
retestDue,
|
|
2155
|
+
epoch
|
|
2156
|
+
);
|
|
1595
2157
|
}
|
|
1596
2158
|
if (!retestDue || reference === void 0) {
|
|
1597
|
-
storage.recordLocalClassification()
|
|
2159
|
+
if (epoch !== dataEpoch || storage.recordLocalClassification(epoch) === null) {
|
|
2160
|
+
return fallbackToReference(
|
|
2161
|
+
input,
|
|
2162
|
+
reference,
|
|
2163
|
+
new SwapAIError(
|
|
2164
|
+
"not_trained",
|
|
2165
|
+
`Classifier "${config.name}" was cleared before returning its result`
|
|
2166
|
+
),
|
|
2167
|
+
retestDue,
|
|
2168
|
+
epoch
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
1598
2171
|
return candidateResult;
|
|
1599
2172
|
}
|
|
1600
2173
|
const referenceResult = await callReference(input, reference);
|
|
1601
|
-
await persistExample(input, referenceResult);
|
|
1602
|
-
|
|
2174
|
+
await persistExample(input, referenceResult, epoch);
|
|
2175
|
+
if (epoch !== dataEpoch) return referenceResult;
|
|
2176
|
+
if (storage.recordLocalClassification(epoch) === null) return referenceResult;
|
|
1603
2177
|
const passed = resultError(config.result, referenceResult, candidateResult) <= config.acceptableError;
|
|
1604
|
-
const failures = storage.recordRetest(passed);
|
|
1605
|
-
if (!passed && failures >= config.retestRevertOn) {
|
|
1606
|
-
await disableModel();
|
|
2178
|
+
const failures = storage.recordRetest(passed, epoch);
|
|
2179
|
+
if (!passed && failures !== null && failures >= config.retestRevertOn) {
|
|
2180
|
+
await disableModel(epoch);
|
|
1607
2181
|
}
|
|
1608
2182
|
return referenceResult;
|
|
1609
2183
|
}
|
|
2184
|
+
async function performDurableClear(clearEpoch) {
|
|
2185
|
+
trained = false;
|
|
2186
|
+
const deletionFailures = [];
|
|
2187
|
+
const throwDeletionFailures = () => {
|
|
2188
|
+
if (deletionFailures.length === 1) throw deletionFailures[0];
|
|
2189
|
+
if (deletionFailures.length > 1) {
|
|
2190
|
+
throw new AggregateError(
|
|
2191
|
+
deletionFailures,
|
|
2192
|
+
`Could not completely clear classifier "${config.name}"`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
const previousModel = loadedModel;
|
|
2197
|
+
if (previousModel !== null) {
|
|
2198
|
+
try {
|
|
2199
|
+
await previousModel.close();
|
|
2200
|
+
if (loadedModel === previousModel) {
|
|
2201
|
+
loadedModel = null;
|
|
2202
|
+
loadedModelEpoch = null;
|
|
2203
|
+
}
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
deletionFailures.push(error);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
const lockWaitStarted = Date.now();
|
|
2209
|
+
let clearState = refreshStoredState();
|
|
2210
|
+
while (clearState.clearPending && clearState.dataEpoch === clearEpoch && (clearState.trainingLeaseOwner !== null && clearState.trainingLeaseEpoch === null && clearState.trainingLeaseUntil !== null && clearState.trainingLeaseUntil > Date.now() || !storage.claimArtifactWriteLock())) {
|
|
2211
|
+
if (Date.now() - lockWaitStarted >= ARTIFACT_LOCK_WAIT_MS) {
|
|
2212
|
+
throw new SwapAIError(
|
|
2213
|
+
"storage_failed",
|
|
2214
|
+
`Timed out waiting to clear classifier "${config.name}" while training was still running`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
await new Promise((resolve2) => setTimeout(resolve2, 10));
|
|
2218
|
+
clearState = refreshStoredState();
|
|
2219
|
+
}
|
|
2220
|
+
if (!clearState.clearPending || clearState.dataEpoch !== clearEpoch) {
|
|
2221
|
+
throwDeletionFailures();
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
try {
|
|
2225
|
+
try {
|
|
2226
|
+
storage.eraseTrainingData(clearEpoch);
|
|
2227
|
+
} catch (error) {
|
|
2228
|
+
deletionFailures.push(error);
|
|
2229
|
+
}
|
|
2230
|
+
clearState = refreshStoredState();
|
|
2231
|
+
if (clearState.clearPending && clearState.dataEpoch === clearEpoch && clearState.clearErased && clearState.clearArtifactGenerationMax !== null) {
|
|
2232
|
+
try {
|
|
2233
|
+
await runtime.clearClassifierArtifactsThroughGeneration(
|
|
2234
|
+
config.name,
|
|
2235
|
+
clearState.clearArtifactGenerationMax
|
|
2236
|
+
);
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
deletionFailures.push(error);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
throwDeletionFailures();
|
|
2242
|
+
storage.finishClearTrainingData(clearEpoch);
|
|
2243
|
+
const current = refreshStoredState();
|
|
2244
|
+
if (!current.clearPending) {
|
|
2245
|
+
clearFailure = null;
|
|
2246
|
+
clearedEpoch = current.dataEpoch;
|
|
2247
|
+
}
|
|
2248
|
+
} finally {
|
|
2249
|
+
storage.releaseArtifactWriteLock();
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function flushNow() {
|
|
2253
|
+
const clearFailureBeforeFlush = clearFailure;
|
|
2254
|
+
await startup;
|
|
2255
|
+
while (true) {
|
|
2256
|
+
const operations = operationQueue;
|
|
2257
|
+
await operations;
|
|
2258
|
+
const training = trainingQueue;
|
|
2259
|
+
await training;
|
|
2260
|
+
if (operations === operationQueue && training === trainingQueue) break;
|
|
2261
|
+
}
|
|
2262
|
+
if (clearFailure !== null && clearFailure !== clearFailureBeforeFlush) {
|
|
2263
|
+
throw clearFailure;
|
|
2264
|
+
}
|
|
2265
|
+
const pending = refreshStoredState();
|
|
2266
|
+
if (pending.clearPending || clearFailureBeforeFlush !== null) {
|
|
2267
|
+
try {
|
|
2268
|
+
await performDurableClear(pending.dataEpoch);
|
|
2269
|
+
if (!refreshStoredState().clearPending) clearFailure = null;
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
rememberClearFailure(error);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (refreshStoredState().clearPending) {
|
|
2275
|
+
clearFailure ??= new SwapAIError(
|
|
2276
|
+
"storage_failed",
|
|
2277
|
+
`Could not completely clear classifier "${config.name}"`
|
|
2278
|
+
);
|
|
2279
|
+
throw clearFailure;
|
|
2280
|
+
}
|
|
2281
|
+
if (clearFailure !== null) throw clearFailure;
|
|
2282
|
+
if (queuedFailure !== null) {
|
|
2283
|
+
const failure = queuedFailure;
|
|
2284
|
+
queuedFailure = null;
|
|
2285
|
+
throw failure;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
1610
2288
|
const classifier = {
|
|
1611
2289
|
isTrained() {
|
|
2290
|
+
assertOpen2(closed || closing);
|
|
2291
|
+
refreshStoredState();
|
|
1612
2292
|
return trained;
|
|
1613
2293
|
},
|
|
1614
2294
|
logClassification(input, result) {
|
|
1615
|
-
assertOpen2(closed);
|
|
2295
|
+
assertOpen2(closed || closing);
|
|
1616
2296
|
const validResult = validateResult(config.result, result);
|
|
1617
|
-
|
|
2297
|
+
const epoch = refreshStoredState().dataEpoch;
|
|
2298
|
+
void persistExample(input, validResult, epoch);
|
|
2299
|
+
},
|
|
2300
|
+
clearTrainingData() {
|
|
2301
|
+
assertOpen2(closed || closing);
|
|
2302
|
+
trained = false;
|
|
2303
|
+
const clearEpoch = storage.beginClearTrainingData();
|
|
2304
|
+
dataEpoch = clearEpoch;
|
|
2305
|
+
const trainingBeforeClear = trainingQueue;
|
|
2306
|
+
const classificationsBeforeClear = classificationQueue;
|
|
2307
|
+
void enqueue(async () => {
|
|
2308
|
+
await startup;
|
|
2309
|
+
await trainingBeforeClear;
|
|
2310
|
+
await classificationsBeforeClear;
|
|
2311
|
+
try {
|
|
2312
|
+
await performDurableClear(clearEpoch);
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
rememberClearFailure(error);
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
1618
2317
|
},
|
|
1619
2318
|
classify(input, reference) {
|
|
1620
|
-
assertOpen2(closed);
|
|
1621
|
-
const
|
|
2319
|
+
assertOpen2(closed || closing);
|
|
2320
|
+
const epoch = refreshStoredState().dataEpoch;
|
|
2321
|
+
const task = classificationQueue.then(
|
|
2322
|
+
() => classifyNow(input, reference, epoch)
|
|
2323
|
+
);
|
|
1622
2324
|
classificationQueue = task.then(
|
|
1623
2325
|
() => void 0,
|
|
1624
2326
|
() => void 0
|
|
@@ -1626,36 +2328,53 @@ function createClassifier(config, storage, runtime) {
|
|
|
1626
2328
|
return task;
|
|
1627
2329
|
},
|
|
1628
2330
|
async flush() {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
const operations = operationQueue;
|
|
1632
|
-
await operations;
|
|
1633
|
-
const training = trainingQueue;
|
|
1634
|
-
await training;
|
|
1635
|
-
if (operations === operationQueue && training === trainingQueue) break;
|
|
1636
|
-
}
|
|
1637
|
-
if (queuedFailure !== null) {
|
|
1638
|
-
const failure = queuedFailure;
|
|
1639
|
-
queuedFailure = null;
|
|
1640
|
-
throw failure;
|
|
1641
|
-
}
|
|
2331
|
+
assertOpen2(closed || closing);
|
|
2332
|
+
await flushNow();
|
|
1642
2333
|
},
|
|
1643
|
-
|
|
1644
|
-
if (closed) return;
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2334
|
+
close() {
|
|
2335
|
+
if (closed) return Promise.resolve();
|
|
2336
|
+
if (closePromise !== null) return closePromise;
|
|
2337
|
+
closing = true;
|
|
2338
|
+
closePromise = (async () => {
|
|
2339
|
+
try {
|
|
2340
|
+
await classificationQueue;
|
|
2341
|
+
await flushNow();
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
closing = false;
|
|
2344
|
+
closePromise = null;
|
|
2345
|
+
throw error;
|
|
2346
|
+
}
|
|
2347
|
+
const failures = [];
|
|
2348
|
+
if (loadedModel !== null) {
|
|
2349
|
+
try {
|
|
2350
|
+
await loadedModel.close();
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
failures.push(error);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
1654
2355
|
loadedModel = null;
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
2356
|
+
loadedModelEpoch = null;
|
|
2357
|
+
try {
|
|
2358
|
+
await runtime.close();
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
failures.push(error);
|
|
2361
|
+
}
|
|
2362
|
+
try {
|
|
2363
|
+
storage.close();
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
failures.push(error);
|
|
2366
|
+
}
|
|
2367
|
+
closed = true;
|
|
2368
|
+
closing = false;
|
|
2369
|
+
if (failures.length === 1) throw failures[0];
|
|
2370
|
+
if (failures.length > 1) {
|
|
2371
|
+
throw new AggregateError(
|
|
2372
|
+
failures,
|
|
2373
|
+
`Could not completely close classifier "${config.name}"`
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
})();
|
|
2377
|
+
return closePromise;
|
|
1659
2378
|
}
|
|
1660
2379
|
};
|
|
1661
2380
|
return classifier;
|