feat: persist isolated utility export jobs
This commit is contained in:
+127
-2
@@ -50,7 +50,7 @@ class FreshStoreTests(StoreTestCase):
|
||||
self.assertTrue(all(slot.media_state == "empty" for slot in slots))
|
||||
self.assertEqual(store.player_observation().phase, "unknown")
|
||||
|
||||
def test_schema_one_database_upgrades_external_reconciliation_atomically(self):
|
||||
def test_schema_one_database_upgrades_reconciliation_and_exports_atomically(self):
|
||||
with self.open_store() as store:
|
||||
database_path = store.path
|
||||
connection = sqlite3.connect(database_path)
|
||||
@@ -67,7 +67,7 @@ class FreshStoreTests(StoreTestCase):
|
||||
with self.open_store() as upgraded:
|
||||
self.assertEqual(
|
||||
upgraded.connection.execute("PRAGMA user_version").fetchone()[0],
|
||||
2,
|
||||
3,
|
||||
)
|
||||
columns = {
|
||||
row["name"]
|
||||
@@ -80,6 +80,131 @@ class FreshStoreTests(StoreTestCase):
|
||||
"SELECT name FROM schema_migrations WHERE version = 2"
|
||||
).fetchone()
|
||||
self.assertEqual(migration["name"], "external_playback_reconciliation")
|
||||
export_migration = upgraded.connection.execute(
|
||||
"SELECT name FROM schema_migrations WHERE version = 3"
|
||||
).fetchone()
|
||||
self.assertEqual(export_migration["name"], "utility_export_jobs")
|
||||
tables = {
|
||||
row["name"]
|
||||
for row in upgraded.connection.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
||||
).fetchall()
|
||||
}
|
||||
self.assertIn("export_jobs", tables)
|
||||
|
||||
def test_schema_two_database_adds_only_utility_export_jobs(self):
|
||||
with self.open_store() as store:
|
||||
database_path = store.path
|
||||
connection = sqlite3.connect(database_path)
|
||||
try:
|
||||
connection.execute("DROP TABLE export_jobs")
|
||||
connection.execute("DELETE FROM schema_migrations WHERE version = 3")
|
||||
connection.execute("PRAGMA user_version = 2")
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
with self.open_store() as upgraded:
|
||||
self.assertEqual(
|
||||
upgraded.connection.execute("PRAGMA user_version").fetchone()[0],
|
||||
3,
|
||||
)
|
||||
self.assertEqual(upgraded.list_export_jobs(), [])
|
||||
|
||||
def test_export_jobs_are_slot_free_bounded_and_durable(self):
|
||||
with self.open_store() as store:
|
||||
first = store.reserve_export_job(
|
||||
"https://www.youtube.com/watch?v=one",
|
||||
"mp4",
|
||||
"Alice",
|
||||
"Sunday",
|
||||
max_active=1,
|
||||
job_id="export-one",
|
||||
)
|
||||
self.assertEqual(first.status, "queued")
|
||||
self.assertEqual(first.job_id, "export-one")
|
||||
self.assertEqual(
|
||||
store.reserve_export_job(
|
||||
"https://www.youtube.com/watch?v=two",
|
||||
"mp3",
|
||||
None,
|
||||
None,
|
||||
max_active=1,
|
||||
).status,
|
||||
"queue_full",
|
||||
)
|
||||
self.assertTrue(store.mark_export_running("export-one"))
|
||||
self.assertTrue(
|
||||
store.complete_export_job(
|
||||
"export-one",
|
||||
("exports/video/export-one/Service.mp4",),
|
||||
)
|
||||
)
|
||||
job = store.get_export_job("export-one")
|
||||
self.assertEqual(job.mode, "mp4")
|
||||
self.assertEqual(job.status, "completed")
|
||||
self.assertEqual(job.submitter, "Alice")
|
||||
self.assertEqual(
|
||||
job.output_paths,
|
||||
("exports/video/export-one/Service.mp4",),
|
||||
)
|
||||
self.assertTrue(all(slot.media_state == "empty" for slot in store.list_slots()))
|
||||
|
||||
def test_restart_marks_live_exports_interrupted_without_touching_slots(self):
|
||||
store = self.open_store()
|
||||
store.reserve_export_job(
|
||||
"https://www.youtube.com/playlist?list=one",
|
||||
"m4a",
|
||||
None,
|
||||
None,
|
||||
max_active=2,
|
||||
job_id="queued-export",
|
||||
)
|
||||
store.reserve_export_job(
|
||||
"https://www.youtube.com/watch?v=two",
|
||||
"mp3",
|
||||
None,
|
||||
None,
|
||||
max_active=2,
|
||||
job_id="running-export",
|
||||
)
|
||||
store.mark_export_running("running-export")
|
||||
store.close()
|
||||
|
||||
with self.open_store() as reopened:
|
||||
jobs = {job.job_id: job for job in reopened.list_export_jobs()}
|
||||
self.assertEqual(jobs["queued-export"].status, "interrupted")
|
||||
self.assertEqual(jobs["running-export"].status, "interrupted")
|
||||
self.assertTrue(
|
||||
all("Controller restart" in job.error for job in jobs.values())
|
||||
)
|
||||
self.assertTrue(
|
||||
all(slot.media_state == "empty" for slot in reopened.list_slots())
|
||||
)
|
||||
|
||||
def test_export_failures_do_not_corrupt_player_inventory(self):
|
||||
with self.open_store() as store:
|
||||
slot = store.reserve_download(
|
||||
"https://www.youtube.com/watch?v=managed",
|
||||
None,
|
||||
None,
|
||||
job_id="managed",
|
||||
)
|
||||
store.fail_download(
|
||||
"managed", slot.slot, slot.generation, "managed failure"
|
||||
)
|
||||
before = store.get_slot(slot.slot)
|
||||
export = store.reserve_export_job(
|
||||
"https://www.youtube.com/watch?v=export",
|
||||
"mp3",
|
||||
None,
|
||||
None,
|
||||
max_active=5,
|
||||
job_id="failed-export",
|
||||
)
|
||||
self.assertEqual(export.status, "queued")
|
||||
self.assertTrue(store.fail_export_job("failed-export", "ffmpeg failed"))
|
||||
self.assertEqual(store.get_slot(slot.slot), before)
|
||||
|
||||
def test_slot_reservation_is_committed_before_worker_transition(self):
|
||||
with self.open_store() as store:
|
||||
|
||||
Reference in New Issue
Block a user