openvibers/openvibe.media Commits
Opening the database no longer fails a clip being cut: getDb() ran the restart recovery ('processing' clips → failed) every time any process opened media.db, so a backfill or operator script run on the host while the service cut a clip failed that clip under it. The recovery stays where the server boots (vod/clip-jobs.start(), which also queues the re-cut); test/db-open-keeps-clips.test.js opens the database from a second process and checks the clip is untouched
Changes
@@@ -32,27 +32,13 @@ function getDb() {ensureObjectTriggers();normalizeThumbnailUrls();seedSettings();-- recoverInterruptedClips();++ // Clips a restart left 'processing' are marked failed (and re-cut) by vod/clip-jobs.start() when++ // the SERVER boots, not here: every script that opens this database (backfills, reconcilers,++ // operator tools) comes through getDb(), and running it while the service is cutting a clip++ // used to fail that clip under it.return database;}--/**-- * A clip is marked 'processing' while ffmpeg cuts it. If the service restarts mid-cut-- * that ffmpeg dies, and nothing ever moves the row on — it stays 'processing' forever,-- * which the player renders as a permanent "the server is cutting your clip".-- *-- * Mark them failed on boot instead: failed is honest, visible, and re-cuttable, whereas-- * 'processing' is a lie that no longer has a process behind it.-- */--function recoverInterruptedClips() {-- try {-- const r = database.prepare(-- "UPDATE clips SET status = 'failed' WHERE status = 'processing'"-- ).run();-- if (r.changes) console.log(`[DB] Recovered ${r.changes} clip(s) left mid-cut by a restart → failed (re-cuttable)`);-- } catch { /* non-fatal */ }--}--// One-time normalization: migrated rows stored old-stack thumbnail URLs// (/api/thumbnails/<name> or absolute variants). Canonical form is /t/<name>// — the legacy redirect route covers stragglers, but direct URLs cache better.nnew file mode 100644@@@ -0,0 +1,38 @@++'use strict';++++// Opening the database (what every script and the service do through getDb()) must not touch a++// clip that is being cut: only the server's own boot (vod/clip-jobs.start()) recovers clips a++// restart left 'processing'. A backfill run on the host used to fail the clip being cut under it.++++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-dbopen-'));++const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };++process.env.DB_PATH = path.join(tmp, 'media.db');++for (const [k, n] of [['VOD_PATH', 'vods'], ['CLIPS_PATH', 'clips'], ['FILES_PATH', 'files'], ['THUMBNAILS_PATH', 'thumbnails'], ['PASTES_PATH', 'pastes'], ['OBJECTS_PATH', 'objects']]) process.env[k] = dir(n);++process.env.MEDIA_PUBLIC_URL = 'https://media.test';++console.log = () => {};++++const Database = require('better-sqlite3');++const db = require('../server/db/database');++db.getDb();++db.run("INSERT INTO clips (id, app_id, vod_id, title, status) VALUES (41, 'live', 1, 'being cut', 'processing')");++++// A second process opening the same file (a script on the host) goes through getDb() too.++const other = require('child_process').spawnSync(process.execPath, ['-e', `++ process.env.DB_PATH = ${JSON.stringify(process.env.DB_PATH)};++ ${['VOD_PATH', 'CLIPS_PATH', 'FILES_PATH', 'THUMBNAILS_PATH', 'PASTES_PATH', 'OBJECTS_PATH', 'MEDIA_PUBLIC_URL'].map(k => `process.env.${k} = ${JSON.stringify(process.env[k])};`).join('\n')}++ console.log = () => {};++ require(${JSON.stringify(path.join(__dirname, '..', 'server', 'db', 'database'))}).getDb();++`], { encoding: 'utf8' });++assert.strictEqual(other.status, 0, other.stderr);++++const raw = new Database(process.env.DB_PATH, { readonly: true });++assert.strictEqual(raw.prepare('SELECT status FROM clips WHERE id = 41').get().status, 'processing', 'opening the database leaves a clip being cut alone');++raw.close();++++fs.rmSync(tmp, { recursive: true, force: true });++process.stdout.write('db open keeps clips: all checks passed\n');
@@ -32,27 +32,13 @@ function getDb() {
@@ -32,27 +32,13 @@ function getDb() {
ensureObjectTriggers();
ensureObjectTriggers();
normalizeThumbnailUrls();
normalizeThumbnailUrls();
seedSettings();
seedSettings();
- recoverInterruptedClips();
+ // Clips a restart left 'processing' are marked failed (and re-cut) by vod/clip-jobs.start() when
+ // the SERVER boots, not here: every script that opens this database (backfills, reconcilers,
+ // operator tools) comes through getDb(), and running it while the service is cutting a clip
+ // used to fail that clip under it.
return database;
return database;
}
}
-/**
- * A clip is marked 'processing' while ffmpeg cuts it. If the service restarts mid-cut
- * that ffmpeg dies, and nothing ever moves the row on — it stays 'processing' forever,
- * which the player renders as a permanent "the server is cutting your clip".
- *
- * Mark them failed on boot instead: failed is honest, visible, and re-cuttable, whereas
- * 'processing' is a lie that no longer has a process behind it.
- */
-function recoverInterruptedClips() {
- try {
- const r = database.prepare(
- "UPDATE clips SET status = 'failed' WHERE status = 'processing'"
- ).run();
- if (r.changes) console.log(`[DB] Recovered ${r.changes} clip(s) left mid-cut by a restart → failed (re-cuttable)`);
- } catch { /* non-fatal */ }
-}
-
// One-time normalization: migrated rows stored old-stack thumbnail URLs
// One-time normalization: migrated rows stored old-stack thumbnail URLs
// (/api/thumbnails/<name> or absolute variants). Canonical form is /t/<name>
// (/api/thumbnails/<name> or absolute variants). Canonical form is /t/<name>
// — the legacy redirect route covers stragglers, but direct URLs cache better.
// — the legacy redirect route covers stragglers, but direct URLs cache better.
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+'use strict';
+
+// Opening the database (what every script and the service do through getDb()) must not touch a
+// clip that is being cut: only the server's own boot (vod/clip-jobs.start()) recovers clips a
+// restart left 'processing'. A backfill run on the host used to fail the clip being cut under it.
+
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-dbopen-'));
+const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
+process.env.DB_PATH = path.join(tmp, 'media.db');
+for (const [k, n] of [['VOD_PATH', 'vods'], ['CLIPS_PATH', 'clips'], ['FILES_PATH', 'files'], ['THUMBNAILS_PATH', 'thumbnails'], ['PASTES_PATH', 'pastes'], ['OBJECTS_PATH', 'objects']]) process.env[k] = dir(n);
+process.env.MEDIA_PUBLIC_URL = 'https://media.test';
+console.log = () => {};
+
+const Database = require('better-sqlite3');
+const db = require('../server/db/database');
+db.getDb();
+db.run("INSERT INTO clips (id, app_id, vod_id, title, status) VALUES (41, 'live', 1, 'being cut', 'processing')");
+
+// A second process opening the same file (a script on the host) goes through getDb() too.
+const other = require('child_process').spawnSync(process.execPath, ['-e', `
+ process.env.DB_PATH = ${JSON.stringify(process.env.DB_PATH)};
+ ${['VOD_PATH', 'CLIPS_PATH', 'FILES_PATH', 'THUMBNAILS_PATH', 'PASTES_PATH', 'OBJECTS_PATH', 'MEDIA_PUBLIC_URL'].map(k => `process.env.${k} = ${JSON.stringify(process.env[k])};`).join('\n')}
+ console.log = () => {};
+ require(${JSON.stringify(path.join(__dirname, '..', 'server', 'db', 'database'))}).getDb();
+`], { encoding: 'utf8' });
+assert.strictEqual(other.status, 0, other.stderr);
+
+const raw = new Database(process.env.DB_PATH, { readonly: true });
+assert.strictEqual(raw.prepare('SELECT status FROM clips WHERE id = 41').get().status, 'processing', 'opening the database leaves a clip being cut alone');
+raw.close();
+
+fs.rmSync(tmp, { recursive: true, force: true });
+process.stdout.write('db open keeps clips: all checks passed\n');