Remove Old Multi-Client Code / Refactoring (#13)

* Remove a lot of old code from render_queue regarding clients

* More code cleanup

* More code cleanup

* Move everything around

* Minor log change
This commit is contained in:
2023-06-11 14:50:20 -05:00
committed by GitHub
parent 86a1dae5b6
commit 94bb1e4362
22 changed files with 66 additions and 210 deletions

View File

@@ -4,3 +4,4 @@ server_log_level: info
flask_log_level: error flask_log_level: error
flask_debug_enable: false flask_debug_enable: false
queue_eval_seconds: 1 queue_eval_seconds: 1
port_number: 8080

View File

@@ -17,7 +17,7 @@ from rich.table import Table
from rich.text import Text from rich.text import Text
from rich.tree import Tree from rich.tree import Tree
from lib.render_workers.base_worker import RenderStatus, string_to_status from lib.workers.base_worker import RenderStatus, string_to_status
from lib.server.server_proxy import RenderServerProxy from lib.server.server_proxy import RenderServerProxy
from lib.utilities.misc_helper import get_time_elapsed from lib.utilities.misc_helper import get_time_elapsed
from start_server import start_server from start_server import start_server

View File

@@ -11,25 +11,13 @@ from tkinter.ttk import Frame, Label, Entry, Combobox, Progressbar
import psutil import psutil
import requests import requests
import threading import threading
from lib.render_workers.blender_worker import Blender from lib.workers.blender_worker import Blender
from lib.server.server_proxy import RenderServerProxy from lib.server.server_proxy import RenderServerProxy
logger = logging.getLogger() logger = logging.getLogger()
prefs_name = 'config/.scheduler_prefs'
label_width = 9 label_width = 9
header_padding = 6 header_padding = 6
server_setup_timeout = 5
def request_data(server_ip, payload, server_port=8080, timeout=2):
try:
req = requests.get(f'http://{server_ip}:{server_port}/api/{payload}', timeout=timeout)
if req.ok:
return req.json()
except Exception as e:
pass
return None
# CheckListBox source - https://stackoverflow.com/questions/50398649/python-tkinter-tk-support-checklist-box # CheckListBox source - https://stackoverflow.com/questions/50398649/python-tkinter-tk-support-checklist-box

View File

@@ -61,7 +61,7 @@ class Blender(BaseRenderEngine):
@classmethod @classmethod
def get_scene_info(cls, project_path, timeout=10): def get_scene_info(cls, project_path, timeout=10):
scene_info = None scene_info = {}
try: try:
results = cls.run_python_script(project_path, os.path.join(os.path.dirname(os.path.realpath(__file__)), results = cls.run_python_script(project_path, os.path.join(os.path.dirname(os.path.realpath(__file__)),
'scripts', 'blender', 'get_file_info.py'), timeout=timeout) 'scripts', 'blender', 'get_file_info.py'), timeout=timeout)
@@ -71,6 +71,8 @@ class Blender(BaseRenderEngine):
raw_data = line.split('SCENE_DATA:')[-1] raw_data = line.split('SCENE_DATA:')[-1]
scene_info = json.loads(raw_data) scene_info = json.loads(raw_data)
break break
elif line.startswith('Error'):
logger.error(f"get_scene_info error: {line.strip()}")
except Exception as e: except Exception as e:
logger.error(f'Error getting file details for .blend file: {e}') logger.error(f'Error getting file details for .blend file: {e}')
return scene_info return scene_info

View File

@@ -1,13 +1,10 @@
import logging import logging
import platform
from datetime import datetime from datetime import datetime
import psutil from sqlalchemy import create_engine
import requests
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from .render_workers.base_worker import RenderStatus, BaseRenderWorker, Base from .workers.base_worker import RenderStatus, BaseRenderWorker, Base
logger = logging.getLogger() logger = logging.getLogger()
@@ -18,27 +15,6 @@ class JobNotFoundError(Exception):
self.job_id = job_id self.job_id = job_id
class RenderClient(Base):
__tablename__ = 'render_clients'
id = Column(Integer, primary_key=True)
hostname = Column(String)
def __init__(self, hostname):
self.hostname = hostname
def is_available(self, timeout=3):
try:
response = requests.get(f"http://{self.hostname}:8080/api/status", timeout=timeout)
if response.ok:
return True
except requests.ConnectionError as e:
pass
return False
def __repr__(self):
return "client stuff"
class RenderQueue: class RenderQueue:
engine = create_engine('sqlite:///database.db') engine = create_engine('sqlite:///database.db')
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
@@ -46,30 +22,19 @@ class RenderQueue:
session = Session() session = Session()
job_queue = [] job_queue = []
maximum_renderer_instances = {'blender': 1, 'aerender': 1, 'ffmpeg': 4} maximum_renderer_instances = {'blender': 1, 'aerender': 1, 'ffmpeg': 4}
hostname = None
port = 8080
client_mode = False
server_hostname = None
last_saved_counts = {} last_saved_counts = {}
def __init__(self): def __init__(self):
pass pass
@classmethod @classmethod
def add_to_render_queue(cls, render_job, force_start=False, client=None): def add_to_render_queue(cls, render_job, force_start=False):
if not client or render_job.client == cls.hostname:
logger.debug('Adding priority {} job to render queue: {}'.format(render_job.priority, render_job)) logger.debug('Adding priority {} job to render queue: {}'.format(render_job.priority, render_job))
render_job.client = cls.hostname
cls.job_queue.append(render_job) cls.job_queue.append(render_job)
if force_start: if force_start:
cls.start_job(render_job) cls.start_job(render_job)
cls.session.add(render_job) cls.session.add(render_job)
cls.save_state() cls.save_state()
else:
# todo: implement client rendering
logger.warning('remote client rendering not implemented yet')
@classmethod @classmethod
def all_jobs(cls): def all_jobs(cls):
@@ -169,75 +134,3 @@ class RenderQueue:
for job_status in RenderStatus: for job_status in RenderStatus:
job_counts[job_status.value] = len(cls.jobs_with_status(job_status)) job_counts[job_status.value] = len(cls.jobs_with_status(job_status))
return job_counts return job_counts
@classmethod
def status(cls):
return {"timestamp": datetime.now().isoformat(),
"platform": platform.platform(),
"cpu_percent": psutil.cpu_percent(percpu=False),
"cpu_percent_per_cpu": psutil.cpu_percent(percpu=True),
"cpu_count": psutil.cpu_count(),
"memory_total": psutil.virtual_memory().total,
"memory_available": psutil.virtual_memory().available,
"memory_percent": psutil.virtual_memory().percent,
"job_counts": cls.job_counts(),
"host_name": cls.hostname
}
@classmethod
def render_clients(cls):
all_clients = cls.session.query(RenderClient).all()
if not all_clients:
cls.session.add(RenderClient(hostname=cls.hostname))
cls.save_state()
all_clients = cls.session.query(RenderClient).all()
return all_clients
@classmethod
def client_with_hostname(cls, hostname):
return cls.session.query(RenderClient).filter(RenderClient.hostname == hostname).first()
@classmethod
def register_client(cls, hostname):
new_client = None
err_msg = None
if hostname == cls.hostname:
err_msg = "Cannot register same hostname as server"
elif cls.client_with_hostname(hostname):
err_msg = f"Client '{hostname}' already registered"
else:
new_client = RenderClient(hostname=hostname)
if not new_client.is_available():
cls.session.add(new_client)
logger.info(f"Client '{hostname}' successfully registered")
cls.save_state()
else:
err_msg = f"Cannot connect to client at hostname: {hostname}"
if err_msg:
logger.warning(err_msg)
return err_msg, 400
else:
return new_client.hostname
@classmethod
def unregister_client(cls, hostname):
success = False
client = cls.client_with_hostname(hostname)
if client and hostname != cls.hostname:
cls.session.delete(client)
cls.save_state()
logger.info(f"Client '{hostname}' successfully unregistered")
success = True
return str(success)
@staticmethod
def is_client_available(client_hostname, timeout=3):
try:
response = requests.get(f"http://{client_hostname}:8080/api/status", timeout=timeout)
if response.ok:
return True
except requests.ConnectionError as e:
pass
return False

View File

@@ -3,28 +3,27 @@ import json
import logging import logging
import os import os
import pathlib import pathlib
import platform
import shutil import shutil
import socket import socket
import threading import threading
import time import time
import zipfile import zipfile
from datetime import datetime from datetime import datetime
from urllib.request import urlretrieve
from zipfile import ZipFile from zipfile import ZipFile
import json2html import json2html
import requests import psutil
import yaml import yaml
from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for, abort from flask import Flask, request, render_template, send_file, after_this_request, Response, redirect, url_for, abort
from urllib.parse import urlparse
from urllib.request import urlretrieve
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from lib.server.zeroconf_server import ZeroconfServer
from lib.render_queue import RenderQueue, JobNotFoundError from lib.render_queue import RenderQueue, JobNotFoundError
from lib.render_workers.worker_factory import RenderWorkerFactory from lib.workers.base_worker import string_to_status, RenderStatus
from lib.render_workers.base_worker import string_to_status, RenderStatus from lib.workers.worker_factory import RenderWorkerFactory
from lib.server.zeroconf_server import ZeroconfServer
from lib.utilities.server_helper import generate_thumbnail_for_job from lib.utilities.server_helper import generate_thumbnail_for_job
from lib.server.server_proxy import RenderServerProxy
logger = logging.getLogger() logger = logging.getLogger()
server = Flask(__name__, template_folder='templates', static_folder='static') server = Flask(__name__, template_folder='templates', static_folder='static')
@@ -55,8 +54,8 @@ def index():
presets = yaml.load(f, Loader=yaml.FullLoader) presets = yaml.load(f, Loader=yaml.FullLoader)
return render_template('index.html', all_jobs=sorted_jobs(RenderQueue.all_jobs()), return render_template('index.html', all_jobs=sorted_jobs(RenderQueue.all_jobs()),
hostname=RenderQueue.hostname, renderer_info=renderer_info(), hostname=server.config['HOSTNAME'], renderer_info=renderer_info(),
render_clients=render_clients(), preset_list=presets) render_clients=[server.config['HOSTNAME']], preset_list=presets)
@server.get('/api/jobs') @server.get('/api/jobs')
@@ -85,7 +84,7 @@ def job_detail(job_id):
media_basename = os.path.basename(found_job.file_list()[0]) media_basename = os.path.basename(found_job.file_list()[0])
media_url = f"/api/job/{job_id}/file/{media_basename}" media_url = f"/api/job/{job_id}/file/{media_basename}"
return render_template('details.html', detail_table=table_html, media_url=media_url, return render_template('details.html', detail_table=table_html, media_url=media_url,
hostname=RenderQueue.hostname, job_status=found_job.status.value.title(), hostname=server.config['HOSTNAME'], job_status=found_job.status.value.title(),
job=found_job, renderer_info=renderer_info()) job=found_job, renderer_info=renderer_info())
@@ -210,23 +209,6 @@ def download_all(job_id):
return f'Cannot find project files for job {job_id}', 500 return f'Cannot find project files for job {job_id}', 500
@server.post('/api/register_client')
def register_client():
client_hostname = request.values['hostname']
return RenderQueue.register_client(client_hostname)
@server.post('/api/unregister_client')
def unregister_client():
client_hostname = request.values['hostname']
return RenderQueue.unregister_client(client_hostname)
@server.get('/api/clients')
def render_clients():
return [c.hostname for c in RenderQueue.render_clients()]
@server.get('/api/presets') @server.get('/api/presets')
def presets(): def presets():
with open('config/presets.yaml') as f: with open('config/presets.yaml') as f:
@@ -239,22 +221,10 @@ def full_status():
full_results = {'timestamp': datetime.now().isoformat(), 'servers': {}} full_results = {'timestamp': datetime.now().isoformat(), 'servers': {}}
try: try:
for client_hostname in render_clients():
is_online = False
if client_hostname == RenderQueue.hostname:
snapshot_results = snapshot() snapshot_results = snapshot()
is_online = True
else:
snapshot_results = {}
try:
snapshot_request = requests.get(f'http://{client_hostname}:8080/snapshot', timeout=1)
snapshot_results = snapshot_request.json()
is_online = snapshot_request.ok
except requests.ConnectionError as e:
pass
server_data = {'status': snapshot_results.get('status', {}), 'jobs': snapshot_results.get('jobs', {}), server_data = {'status': snapshot_results.get('status', {}), 'jobs': snapshot_results.get('jobs', {}),
'is_online': is_online} 'is_online': True}
full_results['servers'][client_hostname] = server_data full_results['servers'][server.config['HOSTNAME']] = server_data
except Exception as e: except Exception as e:
logger.error(f"Exception fetching full status: {e}") logger.error(f"Exception fetching full status: {e}")
@@ -263,7 +233,7 @@ def full_status():
@server.get('/api/snapshot') @server.get('/api/snapshot')
def snapshot(): def snapshot():
server_status = RenderQueue.status() server_status = status()
server_jobs = [x.json() for x in RenderQueue.all_jobs()] server_jobs = [x.json() for x in RenderQueue.all_jobs()]
server_data = {'status': server_status, 'jobs': server_jobs, 'timestamp': datetime.now().isoformat()} server_data = {'status': server_status, 'jobs': server_jobs, 'timestamp': datetime.now().isoformat()}
return server_data return server_data
@@ -385,7 +355,7 @@ def add_job_handler():
input_path=loaded_project_local_path, input_path=loaded_project_local_path,
output_path=job["output_path"], output_path=job["output_path"],
args=job.get('args', {})) args=job.get('args', {}))
render_job.client = job.get('client', None) or RenderQueue.hostname render_job.client = server.config['HOSTNAME']
render_job.owner = job.get("owner", None) render_job.owner = job.get("owner", None)
render_job.name = job.get("name", None) render_job.name = job.get("name", None)
render_job.priority = int(job.get('priority', render_job.priority)) render_job.priority = int(job.get('priority', render_job.priority))
@@ -476,7 +446,18 @@ def clear_history():
@server.route('/api/status') @server.route('/api/status')
def status(): def status():
return RenderQueue.status() return {"timestamp": datetime.now().isoformat(),
"platform": platform.platform(),
"cpu_percent": psutil.cpu_percent(percpu=False),
"cpu_percent_per_cpu": psutil.cpu_percent(percpu=True),
"cpu_count": psutil.cpu_count(),
"memory_total": psutil.virtual_memory().total,
"memory_available": psutil.virtual_memory().available,
"memory_percent": psutil.virtual_memory().percent,
"job_counts": RenderQueue.job_counts(),
"hostname": server.config['HOSTNAME'],
"port": server.config['PORT']
}
@server.get('/api/renderer_info') @server.get('/api/renderer_info')
@@ -495,8 +476,7 @@ def renderer_info():
@server.route('/upload') @server.route('/upload')
def upload_file_page(): def upload_file_page():
return render_template('upload.html', render_clients=render_clients(), return render_template('upload.html', supported_renderers=RenderWorkerFactory.supported_renderers())
supported_renderers=RenderWorkerFactory.supported_renderers())
def start_server(background_thread=False): def start_server(background_thread=False):
@@ -511,15 +491,17 @@ def start_server(background_thread=False):
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S', logging.basicConfig(format='%(asctime)s: %(levelname)s: %(module)s: %(message)s', datefmt='%d-%b-%y %H:%M:%S',
level=config.get('server_log_level', 'INFO').upper()) level=config.get('server_log_level', 'INFO').upper())
# get hostname
local_hostname = socket.gethostname()
local_hostname = local_hostname + (".local" if not local_hostname.endswith(".local") else "")
# load flask settings
server.config['HOSTNAME'] = local_hostname
server.config['PORT'] = int(config.get('port_number', 8080))
server.config['UPLOAD_FOLDER'] = os.path.expanduser(config['upload_folder']) server.config['UPLOAD_FOLDER'] = os.path.expanduser(config['upload_folder'])
server.config['THUMBS_FOLDER'] = os.path.join(os.path.expanduser(config['upload_folder']), 'thumbs') server.config['THUMBS_FOLDER'] = os.path.join(os.path.expanduser(config['upload_folder']), 'thumbs')
server.config['MAX_CONTENT_PATH'] = config['max_content_path'] server.config['MAX_CONTENT_PATH'] = config['max_content_path']
# Get hostname and render clients
local_hostname = socket.gethostname()
RenderQueue.hostname = local_hostname + (".local" if not local_hostname.endswith(".local") else "")
server.config['HOSTNAME'] = RenderQueue.hostname
# disable most Flask logging # disable most Flask logging
flask_log = logging.getLogger('werkzeug') flask_log = logging.getLogger('werkzeug')
flask_log.setLevel(config.get('flask_log_level', 'ERROR').upper()) flask_log.setLevel(config.get('flask_log_level', 'ERROR').upper())
@@ -530,19 +512,18 @@ def start_server(background_thread=False):
thread = threading.Thread(target=eval_loop, kwargs={'delay_sec': config.get('queue_eval_seconds', 1)}, daemon=True) thread = threading.Thread(target=eval_loop, kwargs={'delay_sec': config.get('queue_eval_seconds', 1)}, daemon=True)
thread.start() thread.start()
logging.info(f"Starting Zordon Render Server - Hostname: '{RenderQueue.hostname}'") logging.info(f"Starting Zordon Render Server - Hostname: '{server.config['HOSTNAME']}:'")
zeroconf_server = ZeroconfServer("_zordon._tcp.local.", server.config['HOSTNAME'], server.config['PORT'])
zeroconf_server = ZeroconfServer("_zordon._tcp.local.", RenderQueue.hostname, RenderQueue.port)
zeroconf_server.start() zeroconf_server.start()
try: try:
if background_thread: if background_thread:
server_thread = threading.Thread( server_thread = threading.Thread(
target=lambda: server.run(host='0.0.0.0', port=RenderQueue.port, debug=False, use_reloader=False)) target=lambda: server.run(host='0.0.0.0', port=server.config['PORT'], debug=False, use_reloader=False))
server_thread.start() server_thread.start()
server_thread.join() server_thread.join()
else: else:
server.run(host='0.0.0.0', port=RenderQueue.port, debug=config.get('flask_debug_enable', False), server.run(host='0.0.0.0', port=server.config['PORT'], debug=config.get('flask_debug_enable', False),
use_reloader=False, threaded=True) use_reloader=False, threaded=True)
finally: finally:
zeroconf_server.stop() zeroconf_server.stop()

View File

@@ -4,7 +4,7 @@ import json
import requests import requests
import time import time
import threading import threading
from lib.render_workers.base_worker import RenderStatus from lib.workers.base_worker import RenderStatus
from requests_toolbelt.multipart import MultipartEncoder, MultipartEncoderMonitor from requests_toolbelt.multipart import MultipartEncoder, MultipartEncoderMonitor
status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', RenderStatus.COMPLETED: 'green', status_colors = {RenderStatus.ERROR: "red", RenderStatus.CANCELLED: 'orange1', RenderStatus.COMPLETED: 'green',
@@ -21,7 +21,7 @@ OFFLINE_MAX = 2
class RenderServerProxy: class RenderServerProxy:
def __init__(self, hostname, server_port="8080"): def __init__(self, hostname, server_port="8080"):
self._hostname = hostname self.hostname = hostname
self.port = server_port self.port = server_port
self.fetched_status_data = None self.fetched_status_data = None
self.__jobs_cache_token = None self.__jobs_cache_token = None
@@ -31,15 +31,6 @@ class RenderServerProxy:
self.__offline_flags = 0 self.__offline_flags = 0
self.update_cadence = 5 self.update_cadence = 5
@property
def hostname(self):
return self._hostname
@hostname.setter
def hostname(self, value):
self._hostname = value
self.__jobs_cache_token = None
def connect(self): def connect(self):
status = self.request_data('status') status = self.request_data('status')
return status return status

View File

@@ -1,6 +1,6 @@
import subprocess import subprocess
import ffmpeg # todo: remove all references to ffmpeg library and instead use direct subprocesses import ffmpeg # todo: remove all references to ffmpeg library and instead use direct subprocesses
from ..render_engines.ffmpeg_engine import FFMPEG from ..engines.ffmpeg_engine import FFMPEG
def file_info(path): def file_info(path):

View File

@@ -7,7 +7,7 @@ import threading
import requests import requests
from .ffmpeg_helper import generate_thumbnail, save_first_frame from .ffmpeg_helper import generate_thumbnail, save_first_frame
from lib.render_workers.base_worker import RenderStatus from lib.workers.base_worker import RenderStatus
logger = logging.getLogger() logger = logging.getLogger()

View File

@@ -5,7 +5,7 @@ import re
import time import time
from .base_worker import * from .base_worker import *
from ..render_engines.aerender_engine import AERender from ..engines.aerender_engine import AERender
def aerender_path(): def aerender_path():
paths = glob.glob('/Applications/*After Effects*/aerender') paths = glob.glob('/Applications/*After Effects*/aerender')

View File

@@ -6,7 +6,7 @@ try:
except ImportError: except ImportError:
from base_worker import * from base_worker import *
from ..render_engines.blender_engine import Blender from ..engines.blender_engine import Blender
class BlenderRenderWorker(BaseRenderWorker): class BlenderRenderWorker(BaseRenderWorker):
@@ -30,9 +30,9 @@ class BlenderRenderWorker(BaseRenderWorker):
# Scene Info # Scene Info
self.scene_info = Blender.get_scene_info(input_path) self.scene_info = Blender.get_scene_info(input_path)
self.total_frames = (int(self.scene_info.get('frame_end', 0)) - int(self.scene_info.get('frame_start', 0)) + 1) \ self.total_frames = (int(self.scene_info.get('frame_end', 1)) - int(self.scene_info.get('frame_start', 1)) + 1) \
if self.render_all_frames else 1 if self.render_all_frames else 1
self.current_frame = int(self.scene_info.get('frame_start', 0)) self.current_frame = int(self.scene_info.get('frame_start', 1))
def generate_worker_subprocess(self): def generate_worker_subprocess(self):
@@ -90,7 +90,7 @@ class BlenderRenderWorker(BaseRenderWorker):
time_elapsed, time_remaining)) time_elapsed, time_remaining))
elif "file doesn't exist" in line.lower(): elif "file doesn't exist" in line.lower():
self.log_error(line, halt_render=True) self.log_error(line, halt_render=True)
elif 'error' in line.lower(): elif line.lower().startswith('error'):
self.log_error(line) self.log_error(line)
elif 'Saved' in line or 'Saving' in line or 'quit' in line: elif 'Saved' in line or 'Saving' in line or 'quit' in line:
match = re.match(r'Time: (.*) \(Saving', line) match = re.match(r'Time: (.*) \(Saving', line)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re import re
from .base_worker import * from .base_worker import *
from ..render_engines.ffmpeg_engine import FFMPEG from ..engines.ffmpeg_engine import FFMPEG
class FFMPEGRenderWorker(BaseRenderWorker): class FFMPEGRenderWorker(BaseRenderWorker):

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from lib.server.job_server import start_server from lib.server.api_server import start_server
if __name__ == '__main__': if __name__ == '__main__':
start_server() start_server()