-- ============================================ -- 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'