188 lines
2.8 KiB
JavaScript
188 lines
2.8 KiB
JavaScript
// tools/runFaceClusteringScheduler.js
|
|
const db = require('../db/knex');
|
|
const { spawn } = require('child_process');
|
|
|
|
let running = false;
|
|
|
|
async function getUsersNeedingClustering() {
|
|
|
|
const result = await db.raw(`
|
|
SELECT
|
|
p.user
|
|
FROM faces f
|
|
JOIN photos p
|
|
ON p.id = f.photo_id
|
|
LEFT JOIN (
|
|
SELECT
|
|
user,
|
|
MAX(completed_at) AS last_cluster
|
|
FROM face_cluster_runs
|
|
GROUP BY user
|
|
) r
|
|
ON r.user = p.user
|
|
GROUP BY p.user
|
|
HAVING
|
|
MAX(f.created_at)
|
|
>
|
|
COALESCE(
|
|
MAX(r.last_cluster),
|
|
'1970-01-01'
|
|
)
|
|
`);
|
|
|
|
return Array.isArray(result)
|
|
? result
|
|
: result?.rows || [];
|
|
}
|
|
|
|
function clusterUser(user) {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
console.log(
|
|
'[clusterScheduler] start:',
|
|
user,
|
|
);
|
|
|
|
const child = spawn(
|
|
process.execPath,
|
|
[
|
|
'tools/runFaceClustering.js',
|
|
'--user',
|
|
user,
|
|
'--persist',
|
|
],
|
|
{
|
|
stdio: 'inherit',
|
|
},
|
|
);
|
|
|
|
child.on('exit', (code) => {
|
|
|
|
if (code === 0) {
|
|
console.log(
|
|
'[clusterScheduler] done:',
|
|
user,
|
|
);
|
|
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
reject(
|
|
new Error(
|
|
`Clustering exit code ${code}`,
|
|
),
|
|
);
|
|
});
|
|
|
|
child.on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function clusterUserIfNeeded(user) {
|
|
|
|
const result = await db.raw(
|
|
`
|
|
SELECT
|
|
MAX(f.created_at) AS last_face,
|
|
(
|
|
SELECT MAX(completed_at)
|
|
FROM face_cluster_runs
|
|
WHERE user = ?
|
|
) AS last_cluster
|
|
FROM faces f
|
|
JOIN photos p
|
|
ON p.id = f.photo_id
|
|
WHERE p.user = ?
|
|
`,
|
|
[user, user],
|
|
);
|
|
|
|
const rows =
|
|
Array.isArray(result)
|
|
? result
|
|
: result?.rows || [];
|
|
|
|
const row = rows[0];
|
|
|
|
if (!row) {
|
|
return false;
|
|
}
|
|
|
|
const lastFace =
|
|
row.last_face;
|
|
|
|
const lastCluster =
|
|
row.last_cluster;
|
|
|
|
if (!lastFace) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
lastCluster
|
|
&& new Date(lastFace)
|
|
<= new Date(lastCluster)
|
|
) {
|
|
|
|
console.log(
|
|
'[clusterScheduler] already up to date:',
|
|
user,
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
console.log(
|
|
'[clusterScheduler] clustering required:',
|
|
user,
|
|
);
|
|
|
|
await clusterUser(user);
|
|
|
|
return true;
|
|
}
|
|
|
|
async function runCycle() {
|
|
|
|
if (running) {
|
|
return;
|
|
}
|
|
|
|
running = true;
|
|
|
|
try {
|
|
|
|
const users =
|
|
await getUsersNeedingClustering();
|
|
|
|
if (!users.length) {
|
|
|
|
console.log(
|
|
'[clusterScheduler] no users to cluster',
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
for (const row of users) {
|
|
|
|
const user =
|
|
row.user || row.USER;
|
|
|
|
if (!user) {
|
|
continue;
|
|
}
|
|
|
|
await clusterUser(user);
|
|
}
|
|
|
|
} finally {
|
|
running = false;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
runCycle,
|
|
clusterUserIfNeeded,
|
|
};
|