-- ===================================================================== -- RENDER SCHEMA — Part 1: Farm Nodes & Health -- ===================================================================== SET search_path TO render, public; CREATE TYPE node_status AS ENUM ('Ready','Busy','Offline','Maintenance','Crashed','Updating','Disabled'); CREATE TYPE node_kind AS ENUM ('Shared','Dedicated','Spot'); CREATE TYPE render_type AS ENUM ('Free','Paid','Snapshot','Mockup'); CREATE TYPE ae_version AS ENUM ('2020','2021','2022','2023','2024','2025'); -- --------------------------------------------------------------------- -- render_nodes — registry of farm machines -- --------------------------------------------------------------------- CREATE TABLE render_nodes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, region TEXT NOT NULL, -- 'tehran','frankfurt',... -- Network node_ip INET NOT NULL, worker_port INT NOT NULL DEFAULT 5555, public_endpoint TEXT, -- Spec ram_gb INT, cpu_cores INT, gpu_model TEXT, storage_gb INT, -- Software current_ae_version ae_version NOT NULL, available_ae_versions TEXT[] NOT NULL DEFAULT '{}', node_agent_version TEXT, last_update_at TIMESTAMPTZ, last_update_error TEXT, -- Ownership node_kind node_kind NOT NULL DEFAULT 'Shared', owner_user_id UUID, -- references identity.users (for Dedicated) owner_tenant_id UUID, -- references identity.tenants -- State status node_status NOT NULL DEFAULT 'Offline', current_job_id UUID, -- references render_jobs current_frame_job_id UUID, -- references frame_jobs job_started_at TIMESTAMPTZ, render_type render_type, -- which queue it's serving now -- Health (denormalized for hot reads) last_heartbeat_at TIMESTAMPTZ, last_cpu_pct INT, last_ram_available_mb INT, ae_running BOOLEAN NOT NULL DEFAULT FALSE, -- Stats lifetime_task_count BIGINT NOT NULL DEFAULT 0, lifetime_crash_count INT NOT NULL DEFAULT 0, consecutive_failures INT NOT NULL DEFAULT 0, -- Scheduling priority INT NOT NULL DEFAULT 100, is_active BOOLEAN NOT NULL DEFAULT TRUE, accepts_new_jobs BOOLEAN NOT NULL DEFAULT TRUE, -- Maintenance last_maintenance_at TIMESTAMPTZ, next_maintenance_at TIMESTAMPTZ, maintenance_reason TEXT, -- Local cache state (templates the node has downloaded) cached_template_md5s TEXT[] NOT NULL DEFAULT '{}', cache_used_gb INT NOT NULL DEFAULT 0, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_nodes_region_status ON render_nodes(region, status) WHERE is_active = TRUE; CREATE INDEX idx_nodes_ready ON render_nodes(region, priority DESC) WHERE status = 'Ready' AND accepts_new_jobs = TRUE AND is_active = TRUE; CREATE INDEX idx_nodes_owner ON render_nodes(owner_user_id) WHERE node_kind = 'Dedicated'; CREATE INDEX idx_nodes_heartbeat ON render_nodes(last_heartbeat_at) WHERE is_active = TRUE; CREATE UNIQUE INDEX uq_nodes_ip_port ON render_nodes(node_ip, worker_port); CREATE TRIGGER tg_render_nodes_updated_at BEFORE UPDATE ON render_nodes FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- node_health_logs — historical heartbeat data (partitioned monthly) -- --------------------------------------------------------------------- CREATE TABLE node_health_logs ( id BIGSERIAL, node_id UUID NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), status node_status NOT NULL, cpu_pct INT, ram_available_mb INT, ae_running BOOLEAN, current_job_id UUID, current_frame INT, -- Templates cached (size summary only) cache_used_gb INT, PRIMARY KEY (id, recorded_at) ) PARTITION BY RANGE (recorded_at); CREATE INDEX idx_node_health_node ON node_health_logs(node_id, recorded_at DESC); CREATE TABLE node_health_logs_y2026m01 PARTITION OF node_health_logs FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); -- --------------------------------------------------------------------- -- node_crashes — every detected AE crash -- --------------------------------------------------------------------- CREATE TABLE node_crashes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), node_id UUID NOT NULL REFERENCES render_nodes(id) ON DELETE CASCADE, render_job_id UUID, -- which job was running frame_job_id UUID, crashed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_known_frame INT, crash_signal TEXT, -- exit code or signal error_log TEXT, -- last N lines of AE log log_file_url TEXT, -- MinIO upload of full log -- Recovery auto_recovered BOOLEAN NOT NULL DEFAULT FALSE, recovery_action TEXT, -- 'reset_prefs','restart_ae','reassign_job' recovered_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_node_crashes_node ON node_crashes(node_id, crashed_at DESC); CREATE INDEX idx_node_crashes_job ON node_crashes(render_job_id) WHERE render_job_id IS NOT NULL; -- --------------------------------------------------------------------- -- node_updates — software/AE update tracking -- --------------------------------------------------------------------- CREATE TABLE node_updates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), update_file_name TEXT NOT NULL, update_number INT NOT NULL, description TEXT, target_ae_version ae_version, in_update_queue BOOLEAN NOT NULL DEFAULT FALSE, rolled_out_to_node_ids UUID[] NOT NULL DEFAULT '{}', last_update_queue_date TIMESTAMPTZ, create_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- --------------------------------------------------------------------- -- node_template_cache — what's currently cached on each node -- --------------------------------------------------------------------- CREATE TABLE node_template_cache ( id BIGSERIAL PRIMARY KEY, node_id UUID NOT NULL REFERENCES render_nodes(id) ON DELETE CASCADE, project_id UUID NOT NULL, -- references content.projects aep_file_md5 TEXT NOT NULL, file_size_bytes BIGINT NOT NULL, cached_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), use_count INT NOT NULL DEFAULT 1, local_path TEXT NOT NULL, UNIQUE (node_id, aep_file_md5) ); CREATE INDEX idx_node_cache_node_lru ON node_template_cache(node_id, last_used_at); CREATE INDEX idx_node_cache_md5 ON node_template_cache(aep_file_md5);