255 lines
6.5 KiB
Python
Executable file
255 lines
6.5 KiB
Python
Executable file
import sys
|
|
|
|
import click
|
|
|
|
import frappe
|
|
from frappe.commands import get_site, pass_context
|
|
from frappe.exceptions import SiteNotSpecifiedError
|
|
|
|
|
|
@click.command("trigger-scheduler-event", help="Trigger a scheduler event")
|
|
@click.argument("event")
|
|
@pass_context
|
|
def trigger_scheduler_event(context, event):
|
|
import frappe.utils.scheduler
|
|
|
|
exit_code = 0
|
|
|
|
for site in context.sites:
|
|
try:
|
|
frappe.init(site=site)
|
|
frappe.connect()
|
|
try:
|
|
frappe.get_doc("Scheduled Job Type", {"method": event}).execute()
|
|
except frappe.DoesNotExistError:
|
|
click.secho(f"Event {event} does not exist!", fg="red")
|
|
exit_code = 1
|
|
finally:
|
|
frappe.destroy()
|
|
|
|
if not context.sites:
|
|
raise SiteNotSpecifiedError
|
|
|
|
sys.exit(exit_code)
|
|
|
|
|
|
@click.command("enable-scheduler")
|
|
@pass_context
|
|
def enable_scheduler(context):
|
|
"Enable scheduler"
|
|
import frappe.utils.scheduler
|
|
|
|
for site in context.sites:
|
|
try:
|
|
frappe.init(site=site)
|
|
frappe.connect()
|
|
frappe.utils.scheduler.enable_scheduler()
|
|
frappe.db.commit()
|
|
print("Enabled for", site)
|
|
finally:
|
|
frappe.destroy()
|
|
if not context.sites:
|
|
raise SiteNotSpecifiedError
|
|
|
|
|
|
@click.command("disable-scheduler")
|
|
@pass_context
|
|
def disable_scheduler(context):
|
|
"Disable scheduler"
|
|
import frappe.utils.scheduler
|
|
|
|
for site in context.sites:
|
|
try:
|
|
frappe.init(site=site)
|
|
frappe.connect()
|
|
frappe.utils.scheduler.disable_scheduler()
|
|
frappe.db.commit()
|
|
print("Disabled for", site)
|
|
finally:
|
|
frappe.destroy()
|
|
if not context.sites:
|
|
raise SiteNotSpecifiedError
|
|
|
|
|
|
@click.command("scheduler")
|
|
@click.option("--site", help="site name")
|
|
@click.argument("state", type=click.Choice(["pause", "resume", "disable", "enable", "status"]))
|
|
@click.option(
|
|
"--format", "-f", default="text", type=click.Choice(["json", "text"]), help="Output format"
|
|
)
|
|
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
@pass_context
|
|
def scheduler(context, state: str, format: str, verbose: bool = False, site: str | None = None):
|
|
"""Control scheduler state."""
|
|
import frappe
|
|
from frappe.utils.scheduler import is_scheduler_inactive, toggle_scheduler
|
|
|
|
site = site or get_site(context)
|
|
|
|
output = {
|
|
"text": "Scheduler is {status} for site {site}",
|
|
"json": '{{"status": "{status}", "site": "{site}"}}',
|
|
}
|
|
|
|
with frappe.init_site(site=site):
|
|
match state:
|
|
case "status":
|
|
frappe.connect()
|
|
status = "disabled" if is_scheduler_inactive(verbose=verbose) else "enabled"
|
|
return print(output[format].format(status=status, site=site))
|
|
case "pause" | "resume":
|
|
from frappe.installer import update_site_config
|
|
|
|
update_site_config("pause_scheduler", state == "pause")
|
|
case "enable" | "disable":
|
|
frappe.connect()
|
|
toggle_scheduler(state == "enable")
|
|
frappe.db.commit()
|
|
|
|
print(output[format].format(status=f"{state}d", site=site))
|
|
|
|
|
|
@click.command("set-maintenance-mode")
|
|
@click.option("--site", help="site name")
|
|
@click.argument("state", type=click.Choice(["on", "off"]))
|
|
@pass_context
|
|
def set_maintenance_mode(context, state, site=None):
|
|
"""Put the site in maintenance mode for upgrades."""
|
|
from frappe.installer import update_site_config
|
|
|
|
if not site:
|
|
site = get_site(context)
|
|
|
|
try:
|
|
frappe.init(site=site)
|
|
update_site_config("maintenance_mode", 1 if (state == "on") else 0)
|
|
|
|
finally:
|
|
frappe.destroy()
|
|
|
|
|
|
@click.command(
|
|
"doctor"
|
|
) # Passing context always gets a site and if there is no use site it breaks
|
|
@click.option("--site", help="site name")
|
|
@pass_context
|
|
def doctor(context, site=None):
|
|
"Get diagnostic info about background workers"
|
|
from frappe.utils.doctor import doctor as _doctor
|
|
|
|
if not site:
|
|
site = get_site(context, raise_err=False)
|
|
return _doctor(site=site)
|
|
|
|
|
|
@click.command("show-pending-jobs")
|
|
@click.option("--site", help="site name")
|
|
@pass_context
|
|
def show_pending_jobs(context, site=None):
|
|
"Get diagnostic info about background jobs"
|
|
from frappe.utils.doctor import pending_jobs as _pending_jobs
|
|
|
|
if not site:
|
|
site = get_site(context)
|
|
|
|
with frappe.init_site(site):
|
|
pending_jobs = _pending_jobs(site=site)
|
|
|
|
return pending_jobs
|
|
|
|
|
|
@click.command("purge-jobs")
|
|
@click.option("--site", help="site name")
|
|
@click.option("--queue", default=None, help='one of "low", "default", "high')
|
|
@click.option(
|
|
"--event",
|
|
default=None,
|
|
help='one of "all", "weekly", "monthly", "hourly", "daily", "weekly_long", "daily_long"',
|
|
)
|
|
def purge_jobs(site=None, queue=None, event=None):
|
|
"Purge any pending periodic tasks, if event option is not given, it will purge everything for the site"
|
|
from frappe.utils.doctor import purge_pending_jobs
|
|
|
|
frappe.init(site or "")
|
|
count = purge_pending_jobs(event=event, site=site, queue=queue)
|
|
print(f"Purged {count} jobs")
|
|
|
|
|
|
@click.command("schedule")
|
|
def start_scheduler():
|
|
"""Start scheduler process which is responsible for enqueueing the scheduled job types."""
|
|
from frappe.utils.scheduler import start_scheduler
|
|
|
|
start_scheduler()
|
|
|
|
|
|
@click.command("worker")
|
|
@click.option(
|
|
"--queue",
|
|
type=str,
|
|
help="Queue to consume from. Multiple queues can be specified using comma-separated string. If not specified all queues are consumed.",
|
|
)
|
|
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
|
|
@click.option("-u", "--rq-username", default=None, help="Redis ACL user")
|
|
@click.option("-p", "--rq-password", default=None, help="Redis ACL user password")
|
|
@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.")
|
|
@click.option(
|
|
"--strategy",
|
|
required=False,
|
|
type=click.Choice(["round_robin", "random"]),
|
|
help="Dequeuing strategy to use",
|
|
)
|
|
def start_worker(
|
|
queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None
|
|
):
|
|
"""Start a backgrond worker"""
|
|
from frappe.utils.background_jobs import start_worker
|
|
|
|
start_worker(
|
|
queue,
|
|
quiet=quiet,
|
|
rq_username=rq_username,
|
|
rq_password=rq_password,
|
|
burst=burst,
|
|
strategy=strategy,
|
|
)
|
|
|
|
|
|
@click.command("ready-for-migration")
|
|
@click.option("--site", help="site name")
|
|
@pass_context
|
|
def ready_for_migration(context, site=None):
|
|
from frappe.utils.doctor import get_pending_jobs
|
|
|
|
if not site:
|
|
site = get_site(context)
|
|
|
|
try:
|
|
frappe.init(site=site)
|
|
pending_jobs = get_pending_jobs(site=site)
|
|
|
|
if pending_jobs:
|
|
print(f"NOT READY for migration: site {site} has pending background jobs")
|
|
sys.exit(1)
|
|
|
|
else:
|
|
print(f"READY for migration: site {site} does not have any background jobs")
|
|
return 0
|
|
|
|
finally:
|
|
frappe.destroy()
|
|
|
|
|
|
commands = [
|
|
disable_scheduler,
|
|
doctor,
|
|
enable_scheduler,
|
|
purge_jobs,
|
|
ready_for_migration,
|
|
scheduler,
|
|
set_maintenance_mode,
|
|
show_pending_jobs,
|
|
start_scheduler,
|
|
start_worker,
|
|
trigger_scheduler_event,
|
|
]
|