Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: operation level locking for CLI commands #19162

Merged
merged 1 commit into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions frappe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2214,6 +2214,10 @@ def log_error(title=None, message=None, reference_doctype=None, reference_name=N
title = title or "Error"
traceback = as_unicode(traceback or get_traceback(with_context=True))

if not db:
print(f"Failed to log error in db: {title}")
return

error_log = get_doc(
doctype="Error Log",
error=traceback,
Expand Down
79 changes: 59 additions & 20 deletions frappe/commands/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,43 @@ def restore(
with_private_files=None,
):
"Restore site database from an sql file"

from frappe.utils.synchronization import filelock

site = get_site(context)
frappe.init(site=site)

with filelock("site_restore", timeout=1):
_restore(
site=site,
sql_file_path=sql_file_path,
encryption_key=encryption_key,
db_root_username=db_root_username,
db_root_password=db_root_password,
verbose=context.verbose or verbose,
install_app=install_app,
admin_password=admin_password,
force=context.force or force,
with_public_files=with_public_files,
with_private_files=with_private_files,
)


def _restore(
*,
site=None,
sql_file_path=None,
encryption_key=None,
db_root_username=None,
db_root_password=None,
verbose=None,
install_app=None,
admin_password=None,
force=None,
with_public_files=None,
with_private_files=None,
):

from frappe.installer import (
_new_site,
extract_files,
Expand All @@ -146,10 +183,6 @@ def restore(

_backup = Backup(sql_file_path)

site = get_site(context)
frappe.init(site=site)
force = context.force or force

try:
decompressed_file_name = extract_sql_from_archive(sql_file_path)
if is_partial(decompressed_file_name):
Expand Down Expand Up @@ -211,7 +244,7 @@ def restore(
db_root_username=db_root_username,
db_root_password=db_root_password,
admin_password=admin_password,
verbose=context.verbose,
verbose=verbose,
install_apps=install_app,
source_sql=decompressed_file_name,
force=True,
Expand Down Expand Up @@ -361,6 +394,7 @@ def _reinstall(
site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False
):
from frappe.installer import _new_site
from frappe.utils.synchronization import filelock

if not yes:
click.confirm("This will wipe your database. Are you sure you want to reinstall?", abort=True)
Expand All @@ -378,6 +412,7 @@ def _reinstall(
frappe.destroy()

frappe.init(site=site)

_new_site(
frappe.conf.db_name,
site,
Expand All @@ -398,6 +433,7 @@ def _reinstall(
def install_app(context, apps, force=False):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
from frappe.utils.synchronization import filelock

exit_code = 0

Expand All @@ -408,20 +444,21 @@ def install_app(context, apps, force=False):
frappe.init(site=site)
frappe.connect()

for app in apps:
try:
_install_app(app, verbose=context.verbose, force=force)
except frappe.IncompatibleApp as err:
err_msg = f":\n{err}" if str(err) else ""
print(f"App {app} is Incompatible with Site {site}{err_msg}")
exit_code = 1
except Exception as err:
err_msg = f": {str(err)}\n{frappe.get_traceback()}"
print(f"An error occurred while installing {app}{err_msg}")
exit_code = 1

if not exit_code:
frappe.db.commit()
with filelock("install_app", timeout=1):
for app in apps:
try:
_install_app(app, verbose=context.verbose, force=force)
except frappe.IncompatibleApp as err:
err_msg = f":\n{err}" if str(err) else ""
print(f"App {app} is Incompatible with Site {site}{err_msg}")
exit_code = 1
except Exception as err:
err_msg = f": {str(err)}\n{frappe.get_traceback()}"
print(f"An error occurred while installing {app}{err_msg}")
exit_code = 1

if not exit_code:
frappe.db.commit()

frappe.destroy()

Expand Down Expand Up @@ -791,12 +828,14 @@ def remove_from_installed_apps(context, app):
def uninstall(context, app, dry_run, yes, no_backup, force):
"Remove app and linked modules from site"
from frappe.installer import remove_app
from frappe.utils.synchronization import filelock

for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force)
with filelock("uninstall_app"):
remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force)
finally:
frappe.destroy()
if not context.sites:
Expand Down
42 changes: 22 additions & 20 deletions frappe/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from frappe.coverage import CodeCoverage
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint, update_progress_bar
from frappe.utils.synchronization import filelock

find_executable = which # backwards compatibility
DATA_IMPORT_DEPRECATION = (
Expand Down Expand Up @@ -60,27 +61,28 @@ def build(
if not apps and app:
apps = app

# dont try downloading assets if force used, app specified or running via CI
if not (force or apps or os.environ.get("CI")):
# skip building frappe if assets exist remotely
skip_frappe = download_frappe_assets(verbose=verbose)
else:
skip_frappe = False

# don't minify in developer_mode for faster builds
development = frappe.local.conf.developer_mode or frappe.local.dev_server
mode = "development" if development else "production"
if production:
mode = "production"

if make_copy or restore:
hard_link = make_copy or restore
click.secho(
"bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
fg="yellow",
)
with filelock("bench_build", is_global=True, timeout=10):
# dont try downloading assets if force used, app specified or running via CI
if not (force or apps or os.environ.get("CI")):
# skip building frappe if assets exist remotely
skip_frappe = download_frappe_assets(verbose=verbose)
else:
skip_frappe = False

# don't minify in developer_mode for faster builds
development = frappe.local.conf.developer_mode or frappe.local.dev_server
mode = "development" if development else "production"
if production:
mode = "production"

if make_copy or restore:
hard_link = make_copy or restore
click.secho(
"bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
fg="yellow",
)

bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)


@click.command("watch")
Expand Down
60 changes: 29 additions & 31 deletions frappe/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,40 +76,38 @@ def _new_site(

make_site_dirs()

installing = touch_file(get_site_path("locks", "installing.lock"))

install_db(
root_login=db_root_username,
root_password=db_root_password,
db_name=db_name,
admin_password=admin_password,
verbose=verbose,
source_sql=source_sql,
force=force,
reinstall=reinstall,
db_password=db_password,
db_type=db_type,
db_host=db_host,
db_port=db_port,
no_mariadb_socket=no_mariadb_socket,
)
apps_to_install = (
["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
)
with filelock("bench_new_site", timeout=1):
install_db(
root_login=db_root_username,
root_password=db_root_password,
db_name=db_name,
admin_password=admin_password,
verbose=verbose,
source_sql=source_sql,
force=force,
reinstall=reinstall,
db_password=db_password,
db_type=db_type,
db_host=db_host,
db_port=db_port,
no_mariadb_socket=no_mariadb_socket,
)

for app in apps_to_install:
# NOTE: not using force here for 2 reasons:
# 1. It's not really needed here as we've freshly installed a new db
# 2. If someone uses a sql file to do restore and that file already had
# installed_apps then it might cause problems as that sql file can be of any previous version(s)
# which might be incompatible with the current version and using force might cause problems.
# Example: the DocType DocType might not have `migration_hash` column which will cause failure in the restore.
install_app(app, verbose=verbose, set_as_patched=not source_sql, force=False)
apps_to_install = (
["frappe"] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
)

os.remove(installing)
for app in apps_to_install:
# NOTE: not using force here for 2 reasons:
# 1. It's not really needed here as we've freshly installed a new db
# 2. If someone uses a sql file to do restore and that file already had
# installed_apps then it might cause problems as that sql file can be of any previous version(s)
# which might be incompatible with the current version and using force might cause problems.
# Example: the DocType DocType might not have `migration_hash` column which will cause failure in the restore.
install_app(app, verbose=verbose, set_as_patched=not source_sql, force=False)

scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()

scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
Expand Down
18 changes: 10 additions & 8 deletions frappe/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.utils.fixtures import sync_fixtures
from frappe.utils.synchronization import filelock
from frappe.website.utils import clear_website_cache

BENCH_START_MESSAGE = dedent(
Expand Down Expand Up @@ -169,11 +170,12 @@ def run(self, site: str):
if not self.required_services_running():
raise SystemExit(1)

self.setUp()
try:
self.pre_schema_updates()
self.run_schema_updates()
self.post_schema_updates()
finally:
self.tearDown()
frappe.destroy()
with filelock("bench_migrate", timeout=1):
self.setUp()
try:
self.pre_schema_updates()
self.run_schema_updates()
self.post_schema_updates()
finally:
self.tearDown()
frappe.destroy()
2 changes: 2 additions & 0 deletions frappe/search/website_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import frappe
from frappe.search.full_text_search import FullTextSearch
from frappe.utils import set_request, update_progress_bar
from frappe.utils.synchronization import filelock
from frappe.website.serve import get_response_content

INDEX_NAME = "web_routes"
Expand Down Expand Up @@ -140,6 +141,7 @@ def remove_document_from_index(path):
return ws.remove_document_from_index(path)


@filelock("building_website_search")
def build_index_for_all_routes():
ws = WebsiteSearch(INDEX_NAME)
return ws.build()
2 changes: 1 addition & 1 deletion frappe/utils/synchronization.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def filelock(lock_name: str, *, timeout=30, is_global=False):
frappe.log_error("Filelock: Failed to aquire {lock_path}")

raise LockTimeoutError(
_("Failed to aquire lock: {}").format(lock_name)
_("Failed to aquire lock: {}. Lock may be held by another process.").format(lock_name)
+ "<br>"
+ _("You can manually remove the lock if you think it's safe: {}").format(lock_path)
) from e