@swapai/core 0.2.2 → 0.3.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/dist/index.js CHANGED
@@ -1120,6 +1120,8 @@ var SCHEMA = `
1120
1120
  new_examples_since_training INTEGER NOT NULL DEFAULT 0,
1121
1121
  training_attempts INTEGER NOT NULL DEFAULT 0,
1122
1122
  examples_used_for_training INTEGER NOT NULL DEFAULT 0,
1123
+ last_evaluated_error REAL,
1124
+ last_evaluated_at INTEGER,
1123
1125
  local_classifications_since_retest INTEGER NOT NULL DEFAULT 0,
1124
1126
  total_local_classifications INTEGER NOT NULL DEFAULT 0,
1125
1127
  total_retests INTEGER NOT NULL DEFAULT 0,
@@ -1165,6 +1167,19 @@ var SCHEMA = `
1165
1167
  CREATE INDEX IF NOT EXISTS examples_by_split
1166
1168
  ON examples(classifier_name, generation, split, id);
1167
1169
 
1170
+ CREATE TABLE IF NOT EXISTS classifier_runtimes (
1171
+ classifier_name TEXT NOT NULL,
1172
+ runtime_id TEXT NOT NULL,
1173
+ pid INTEGER NOT NULL,
1174
+ started_at INTEGER NOT NULL,
1175
+ heartbeat_at INTEGER NOT NULL,
1176
+ PRIMARY KEY (classifier_name, runtime_id),
1177
+ FOREIGN KEY (classifier_name) REFERENCES classifiers(name) ON DELETE CASCADE
1178
+ );
1179
+
1180
+ CREATE INDEX IF NOT EXISTS classifier_runtimes_by_heartbeat
1181
+ ON classifier_runtimes(classifier_name, heartbeat_at);
1182
+
1168
1183
  CREATE TRIGGER IF NOT EXISTS swapai_v2_classifiers_insert
1169
1184
  BEFORE INSERT ON classifiers
1170
1185
  WHEN swapai_writer_version() < 2
@@ -1306,6 +1321,8 @@ function openStorage(options) {
1306
1321
  c.new_examples_since_training,
1307
1322
  c.training_attempts,
1308
1323
  c.examples_used_for_training,
1324
+ c.last_evaluated_error,
1325
+ c.last_evaluated_at,
1309
1326
  c.local_classifications_since_retest,
1310
1327
  c.total_local_classifications,
1311
1328
  c.total_retests,
@@ -1341,6 +1358,8 @@ function openStorage(options) {
1341
1358
  newExamplesSinceTraining: row2.new_examples_since_training,
1342
1359
  trainingAttempts: row2.training_attempts,
1343
1360
  examplesUsedForTraining: row2.examples_used_for_training,
1361
+ lastEvaluatedError: row2.last_evaluated_error,
1362
+ lastEvaluatedAt: row2.last_evaluated_at,
1344
1363
  localClassificationsSinceRetest: row2.local_classifications_since_retest,
1345
1364
  totalLocalClassifications: row2.total_local_classifications,
1346
1365
  totalRetests: row2.total_retests,
@@ -1439,6 +1458,21 @@ function openStorage(options) {
1439
1458
  `).run(exampleCount, Date.now(), options.name, epoch);
1440
1459
  return result.changes === 1;
1441
1460
  },
1461
+ recordEvaluation(error, expectedDataEpoch) {
1462
+ assertOpen(closed);
1463
+ if (!Number.isFinite(error) || error < 0 || error > 1) {
1464
+ throw new TypeError("evaluation error must be between 0 and 1");
1465
+ }
1466
+ const epoch = expectedDataEpoch ?? storage.snapshot().dataEpoch;
1467
+ const result = database.prepare(`
1468
+ UPDATE classifiers
1469
+ SET last_evaluated_error = ?,
1470
+ last_evaluated_at = ?,
1471
+ updated_at = ?
1472
+ WHERE name = ? AND data_epoch = ? AND clear_pending = 0
1473
+ `).run(error, Date.now(), Date.now(), options.name, epoch);
1474
+ return result.changes === 1;
1475
+ },
1442
1476
  promoteGeneration(model, expectedDataEpoch) {
1443
1477
  assertOpen(closed);
1444
1478
  let generation = 0;
@@ -1522,6 +1556,52 @@ function openStorage(options) {
1522
1556
  WHERE name = ? AND training_lease_owner = ?
1523
1557
  `).run(Date.now(), options.name, owner);
1524
1558
  },
1559
+ registerRuntime(runtimeId, pid) {
1560
+ assertOpen(closed);
1561
+ if (runtimeId.trim() === "" || !Number.isSafeInteger(pid) || pid <= 0) {
1562
+ throw new TypeError("runtime needs an id and positive process id");
1563
+ }
1564
+ const now = Date.now();
1565
+ database.prepare(`
1566
+ INSERT INTO classifier_runtimes (
1567
+ classifier_name, runtime_id, pid, started_at, heartbeat_at
1568
+ ) VALUES (?, ?, ?, ?, ?)
1569
+ ON CONFLICT(classifier_name, runtime_id) DO UPDATE SET
1570
+ pid = excluded.pid,
1571
+ heartbeat_at = excluded.heartbeat_at
1572
+ `).run(options.name, runtimeId, pid, now, now);
1573
+ },
1574
+ heartbeatRuntime(runtimeId) {
1575
+ assertOpen(closed);
1576
+ database.prepare(`
1577
+ UPDATE classifier_runtimes
1578
+ SET heartbeat_at = ?
1579
+ WHERE classifier_name = ? AND runtime_id = ?
1580
+ `).run(Date.now(), options.name, runtimeId);
1581
+ },
1582
+ removeRuntime(runtimeId) {
1583
+ assertOpen(closed);
1584
+ database.prepare(`
1585
+ DELETE FROM classifier_runtimes
1586
+ WHERE classifier_name = ? AND runtime_id = ?
1587
+ `).run(options.name, runtimeId);
1588
+ },
1589
+ listLiveRuntimes(heartbeatAfter) {
1590
+ assertOpen(closed);
1591
+ if (!Number.isFinite(heartbeatAfter)) {
1592
+ throw new TypeError("heartbeat cutoff must be finite");
1593
+ }
1594
+ return database.prepare(`
1595
+ SELECT
1596
+ runtime_id AS runtimeId,
1597
+ pid,
1598
+ started_at AS startedAt,
1599
+ heartbeat_at AS heartbeatAt
1600
+ FROM classifier_runtimes
1601
+ WHERE classifier_name = ? AND heartbeat_at >= ?
1602
+ ORDER BY started_at, runtime_id
1603
+ `).all(options.name, heartbeatAfter).map((value) => row(value));
1604
+ },
1525
1605
  archiveAndReset(expectedDataEpoch, resetOptions) {
1526
1606
  assertOpen(closed);
1527
1607
  let nextGeneration = 0;
@@ -1563,6 +1643,8 @@ function openStorage(options) {
1563
1643
  new_examples_since_training = ?,
1564
1644
  training_attempts = 0,
1565
1645
  examples_used_for_training = 0,
1646
+ last_evaluated_error = NULL,
1647
+ last_evaluated_at = NULL,
1566
1648
  training_lease_owner = NULL,
1567
1649
  training_lease_until = NULL,
1568
1650
  training_lease_epoch = NULL,
@@ -1630,6 +1712,8 @@ function openStorage(options) {
1630
1712
  new_examples_since_training = 0,
1631
1713
  training_attempts = 0,
1632
1714
  examples_used_for_training = 0,
1715
+ last_evaluated_error = NULL,
1716
+ last_evaluated_at = NULL,
1633
1717
  local_classifications_since_retest = 0,
1634
1718
  total_local_classifications = 0,
1635
1719
  total_retests = 0,
@@ -1792,6 +1876,16 @@ function migrateClassifierColumns(database) {
1792
1876
  "ALTER TABLE classifiers ADD COLUMN training_lease_epoch INTEGER"
1793
1877
  );
1794
1878
  }
1879
+ if (!columns.has("last_evaluated_error")) {
1880
+ database.exec(
1881
+ "ALTER TABLE classifiers ADD COLUMN last_evaluated_error REAL"
1882
+ );
1883
+ }
1884
+ if (!columns.has("last_evaluated_at")) {
1885
+ database.exec(
1886
+ "ALTER TABLE classifiers ADD COLUMN last_evaluated_at INTEGER"
1887
+ );
1888
+ }
1795
1889
  });
1796
1890
  }
1797
1891
  function generationByNumber(database, name, generation) {
@@ -1925,6 +2019,7 @@ function requiredRow(value) {
1925
2019
  // src/classifier.ts
1926
2020
  var TRAINING_LEASE_DURATION_MS = 5 * 60 * 1e3;
1927
2021
  var TRAINING_LEASE_RENEWAL_MS = 60 * 1e3;
2022
+ var RUNTIME_HEARTBEAT_MS = 2e3;
1928
2023
  var ARTIFACT_LOCK_WAIT_MS = 3e4;
1929
2024
  function init(inputConfig) {
1930
2025
  const config = normalizeConfig(inputConfig);
@@ -1974,6 +2069,18 @@ function createClassifier(config, storage, runtime) {
1974
2069
  let trainingQueue = Promise.resolve();
1975
2070
  let trainingScheduled = false;
1976
2071
  const trainingLeaseOwner = `${process.pid}:${randomUUID2()}`;
2072
+ storage.registerRuntime(trainingLeaseOwner, process.pid);
2073
+ const runtimeHeartbeat = setInterval(() => {
2074
+ try {
2075
+ storage.heartbeatRuntime(trainingLeaseOwner);
2076
+ } catch (error) {
2077
+ reportBackgroundError(
2078
+ config,
2079
+ toSwapAIError(error, "storage_failed", "Could not update classifier status")
2080
+ );
2081
+ }
2082
+ }, RUNTIME_HEARTBEAT_MS);
2083
+ runtimeHeartbeat.unref();
1977
2084
  let classificationQueue = Promise.resolve();
1978
2085
  let queuedFailure = null;
1979
2086
  let clearFailure = null;
@@ -2199,7 +2306,15 @@ function createClassifier(config, storage, runtime) {
2199
2306
  candidate: candidateResult
2200
2307
  });
2201
2308
  }
2202
- if (averageError(config.result, comparisons) > config.acceptableError) {
2309
+ const measuredError = averageError(config.result, comparisons);
2310
+ if (!storage.recordEvaluation(measuredError, trainingEpoch)) {
2311
+ await runtime.clearClassifierGenerationArtifacts(
2312
+ config.name,
2313
+ snapshot.activeGeneration
2314
+ );
2315
+ return false;
2316
+ }
2317
+ if (measuredError > config.acceptableError) {
2203
2318
  return true;
2204
2319
  }
2205
2320
  if (!renewTrainingLease()) {
@@ -2582,6 +2697,8 @@ function createClassifier(config, storage, runtime) {
2582
2697
  failures.push(error);
2583
2698
  }
2584
2699
  try {
2700
+ clearInterval(runtimeHeartbeat);
2701
+ storage.removeRuntime(trainingLeaseOwner);
2585
2702
  storage.close();
2586
2703
  } catch (error) {
2587
2704
  failures.push(error);