Complete multi-instance support: Phases 3–6 & deployment
- 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.
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
-- ============================================
|
||||
-- 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'
|
||||
+3
-5
@@ -265,13 +265,11 @@ BEGIN
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 10
|
||||
LOOP
|
||||
RAISE NOTICE 'Calls: %, Avg: %ms, Max: %ms, Total: %ms, %%: %, Query: %',
|
||||
RAISE NOTICE 'Calls: %, Avg: %ms, Max: %ms, Total: %ms, Percent: %%',
|
||||
query_rec.calls,
|
||||
query_rec.avg_time_ms,
|
||||
query_rec.max_time_ms,
|
||||
query_rec.total_time_ms,
|
||||
query_rec.percent_total,
|
||||
query_rec.query_preview;
|
||||
query_rec.total_time_ms;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -341,7 +339,7 @@ BEGIN
|
||||
FROM pg_stat_database WHERE datname = 'jellyfin';
|
||||
|
||||
IF hit_ratio < 95 THEN
|
||||
RAISE WARNING 'Low cache hit ratio: %%. Target: >95%%', hit_ratio;
|
||||
RAISE WARNING 'Low cache hit ratio: %. Target: >95', hit_ratio;
|
||||
RAISE NOTICE 'RECOMMENDATION: Increase shared_buffers and effective_cache_size';
|
||||
END IF;
|
||||
|
||||
|
||||
@@ -191,9 +191,9 @@ BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'activitylog'
|
||||
AND table_name = 'ActivityLog'
|
||||
AND table_name = 'ActivityLogs'
|
||||
) THEN
|
||||
ANALYZE activitylog."ActivityLog";
|
||||
ANALYZE activitylog."ActivityLogs";
|
||||
RAISE NOTICE 'ActivityLog analyzed';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
Reference in New Issue
Block a user