77e30685bb
- Implements Phases 3–6: session isolation, cache coordination, primary election, and file system monitor coordination for Jellyfin with PostgreSQL. - Adds new database entities (Instance, DistributedLock, FileSystemChange) and EF model configurations. - Includes SQL migration scripts and EF migration for all required tables, columns, and helper functions. - Updates Device entity and JellyfinDbContext for multi-instance tracking. - Integrates new DI services for instance registry, distributed locks, cache coordinator, and primary election. - Adds publishing profiles (Win/Linux/FrameworkDependent) and automation script for deployment. - Extensive documentation for architecture, setup, and publishing. - All changes are backward compatible and build successfully.
207 lines
7.6 KiB
PL/PgSQL
207 lines
7.6 KiB
PL/PgSQL
-- ============================================
|
|
-- Multi-Instance Support Migration
|
|
-- Phase 1: Instance Registration
|
|
-- Phase 2: Distributed Locking
|
|
-- Phase 6: File System Monitor Coordination
|
|
-- ============================================
|
|
|
|
-- Run this script to add multi-instance support to existing database
|
|
-- Usage: psql -U jellyfin -d jellyfin -f add_multi_instance_support.sql
|
|
|
|
BEGIN;
|
|
|
|
-- 1. Create Instances table
|
|
CREATE TABLE IF NOT EXISTS library."Instances" (
|
|
"InstanceId" UUID PRIMARY KEY,
|
|
"Hostname" VARCHAR(255) NOT NULL,
|
|
"ProcessId" INTEGER NOT NULL,
|
|
"HttpPort" INTEGER NOT NULL,
|
|
"HttpsPort" INTEGER,
|
|
"Version" VARCHAR(50) NOT NULL,
|
|
"StartedAt" TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
"LastHeartbeat" TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
"Status" VARCHAR(50) NOT NULL DEFAULT 'Active',
|
|
"IsPrimary" BOOLEAN NOT NULL DEFAULT FALSE,
|
|
"Capabilities" TEXT NOT NULL DEFAULT '{}',
|
|
"Configuration" TEXT NOT NULL DEFAULT '{}',
|
|
"RowVersion" INTEGER NOT NULL DEFAULT 0,
|
|
CONSTRAINT chk_instance_status CHECK ("Status" IN ('Active', 'Shutdown', 'Failed', 'Maintenance'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_instances_lastheartbeat ON library."Instances"("LastHeartbeat");
|
|
CREATE INDEX IF NOT EXISTS idx_instances_status ON library."Instances"("Status");
|
|
CREATE INDEX IF NOT EXISTS idx_instances_isprimary ON library."Instances"("IsPrimary") WHERE "IsPrimary" = TRUE;
|
|
CREATE INDEX IF NOT EXISTS idx_instances_host_process ON library."Instances"("Hostname", "ProcessId");
|
|
|
|
-- 2. Create Distributed Locks table
|
|
CREATE TABLE IF NOT EXISTS library."DistributedLocks" (
|
|
"LockName" VARCHAR(255) PRIMARY KEY,
|
|
"InstanceId" UUID NOT NULL,
|
|
"AcquiredAt" TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
"ExpiresAt" TIMESTAMP NOT NULL,
|
|
"RenewedAt" TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
"RowVersion" INTEGER NOT NULL DEFAULT 0,
|
|
CONSTRAINT fk_lock_instance FOREIGN KEY ("InstanceId")
|
|
REFERENCES library."Instances"("InstanceId") ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_locks_expiration ON library."DistributedLocks"("ExpiresAt");
|
|
CREATE INDEX IF NOT EXISTS idx_locks_instance ON library."DistributedLocks"("InstanceId");
|
|
CREATE INDEX IF NOT EXISTS idx_locks_expiration_instance ON library."DistributedLocks"("ExpiresAt", "InstanceId");
|
|
|
|
-- 3. Add InstanceId to ActivityLog
|
|
ALTER TABLE activitylog."ActivityLog" ADD COLUMN IF NOT EXISTS "InstanceId" UUID;
|
|
CREATE INDEX IF NOT EXISTS idx_activitylog_instance ON activitylog."ActivityLog"("InstanceId");
|
|
|
|
-- 4. Add InstanceId to Devices for session tracking
|
|
ALTER TABLE library."Devices" ADD COLUMN IF NOT EXISTS "InstanceId" UUID;
|
|
CREATE INDEX IF NOT EXISTS idx_devices_instance ON library."Devices"("InstanceId");
|
|
|
|
-- 5. Create File System Changes table (Phase 6)
|
|
CREATE TABLE IF NOT EXISTS library."FileSystemChanges" (
|
|
"Id" BIGSERIAL PRIMARY KEY,
|
|
"Path" TEXT NOT NULL,
|
|
"ChangeType" VARCHAR(50) NOT NULL,
|
|
"DetectedAt" TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
"DetectedBy" UUID NOT NULL,
|
|
"ProcessedAt" TIMESTAMP,
|
|
"ProcessedBy" UUID,
|
|
"LibraryId" UUID,
|
|
"Error" TEXT,
|
|
"OldPath" TEXT,
|
|
CONSTRAINT fk_fschange_detectedby FOREIGN KEY ("DetectedBy")
|
|
REFERENCES library."Instances"("InstanceId") ON DELETE CASCADE,
|
|
CONSTRAINT fk_fschange_processedby FOREIGN KEY ("ProcessedBy")
|
|
REFERENCES library."Instances"("InstanceId") ON DELETE SET NULL,
|
|
CONSTRAINT chk_filesystemchange_type CHECK ("ChangeType" IN ('Created', 'Modified', 'Deleted', 'Renamed'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_filesystemchanges_unprocessed ON library."FileSystemChanges"("ProcessedAt")
|
|
WHERE "ProcessedAt" IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_filesystemchanges_detectedat ON library."FileSystemChanges"("DetectedAt");
|
|
CREATE INDEX IF NOT EXISTS idx_filesystemchanges_path ON library."FileSystemChanges"("Path");
|
|
CREATE INDEX IF NOT EXISTS idx_filesystemchanges_library ON library."FileSystemChanges"("LibraryId")
|
|
WHERE "LibraryId" IS NOT NULL;
|
|
|
|
-- 6. Function to cleanup stale instances
|
|
CREATE OR REPLACE FUNCTION library.cleanup_stale_instances()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
updated_count INTEGER;
|
|
BEGIN
|
|
UPDATE library."Instances"
|
|
SET "Status" = 'Failed'
|
|
WHERE "Status" = 'Active'
|
|
AND "LastHeartbeat" < NOW() - INTERVAL '2 minutes';
|
|
|
|
GET DIAGNOSTICS updated_count = ROW_COUNT;
|
|
RETURN updated_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- 7. Function to get primary instance
|
|
CREATE OR REPLACE FUNCTION library.get_primary_instance()
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
primary_id UUID;
|
|
BEGIN
|
|
SELECT "InstanceId" INTO primary_id
|
|
FROM library."Instances"
|
|
WHERE "Status" = 'Active'
|
|
AND "IsPrimary" = TRUE
|
|
AND "LastHeartbeat" > NOW() - INTERVAL '1 minute'
|
|
LIMIT 1;
|
|
|
|
RETURN primary_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- 8. Function to elect primary instance
|
|
CREATE OR REPLACE FUNCTION library.elect_primary_instance()
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
elected_id UUID;
|
|
BEGIN
|
|
-- Clear any existing primary that's not active
|
|
UPDATE library."Instances"
|
|
SET "IsPrimary" = FALSE
|
|
WHERE "IsPrimary" = TRUE
|
|
AND ("Status" != 'Active' OR "LastHeartbeat" < NOW() - INTERVAL '1 minute');
|
|
|
|
-- Check if we have an active primary
|
|
SELECT "InstanceId" INTO elected_id
|
|
FROM library."Instances"
|
|
WHERE "Status" = 'Active'
|
|
AND "IsPrimary" = TRUE
|
|
AND "LastHeartbeat" > NOW() - INTERVAL '1 minute'
|
|
LIMIT 1;
|
|
|
|
-- If no primary, elect the oldest active instance
|
|
IF elected_id IS NULL THEN
|
|
SELECT "InstanceId" INTO elected_id
|
|
FROM library."Instances"
|
|
WHERE "Status" = 'Active'
|
|
AND "LastHeartbeat" > NOW() - INTERVAL '1 minute'
|
|
ORDER BY "StartedAt" ASC
|
|
LIMIT 1;
|
|
|
|
IF elected_id IS NOT NULL THEN
|
|
UPDATE library."Instances"
|
|
SET "IsPrimary" = TRUE
|
|
WHERE "InstanceId" = elected_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN elected_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- 9. Function to cleanup old processed file system changes
|
|
CREATE OR REPLACE FUNCTION library.cleanup_old_filesystem_changes()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
deleted_count INTEGER;
|
|
BEGIN
|
|
-- Delete processed changes older than 7 days
|
|
DELETE FROM library."FileSystemChanges"
|
|
WHERE "ProcessedAt" IS NOT NULL
|
|
AND "ProcessedAt" < NOW() - INTERVAL '7 days';
|
|
|
|
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
|
RETURN deleted_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- 10. Grant permissions
|
|
GRANT ALL ON TABLE library."Instances" TO jellyfin;
|
|
GRANT ALL ON TABLE library."DistributedLocks" TO jellyfin;
|
|
GRANT ALL ON TABLE library."FileSystemChanges" TO jellyfin;
|
|
GRANT ALL ON SEQUENCE library."FileSystemChanges_Id_seq" TO jellyfin;
|
|
GRANT EXECUTE ON FUNCTION library.cleanup_stale_instances() TO jellyfin;
|
|
GRANT EXECUTE ON FUNCTION library.get_primary_instance() TO jellyfin;
|
|
GRANT EXECUTE ON FUNCTION library.elect_primary_instance() TO jellyfin;
|
|
GRANT EXECUTE ON FUNCTION library.cleanup_old_filesystem_changes() TO jellyfin;
|
|
|
|
COMMIT;
|
|
|
|
-- Verification queries
|
|
\echo '--- Verification ---'
|
|
\echo 'Checking Instances table...'
|
|
SELECT COUNT(*) AS instance_count FROM library."Instances";
|
|
|
|
\echo 'Checking DistributedLocks table...'
|
|
SELECT COUNT(*) AS lock_count FROM library."DistributedLocks";
|
|
|
|
\echo 'Checking FileSystemChanges table...'
|
|
SELECT COUNT(*) AS fschange_count FROM library."FileSystemChanges";
|
|
|
|
\echo 'Checking Devices table for InstanceId column...'
|
|
SELECT COUNT(*) AS devices_with_instance FROM library."Devices" WHERE "InstanceId" IS NOT NULL;
|
|
|
|
\echo 'Testing cleanup function...'
|
|
SELECT library.cleanup_stale_instances() AS stale_instances_cleaned;
|
|
|
|
\echo ''
|
|
\echo '✅ Multi-instance support tables and functions created successfully!'
|
|
\echo 'Next step: Start Jellyfin instances with EnableMultiInstance=true in startup.json'
|