Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion specifyweb/backend/setup_tool/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

import json
from typing import Optional
from datetime import timedelta
from django.http import (JsonResponse)
from django.db.models import Max
from django.db import transaction
from django.utils import timezone

from specifyweb.backend.permissions.models import UserPolicy

Expand Down Expand Up @@ -39,6 +41,7 @@

APP_VERSION = "7"
SCHEMA_VERSION = "2.10"
CONFIG_TASK_COLLECTION_BLOCK_WINDOW = timedelta(minutes=15)

class SetupError(Exception):
"""Raised by any setup tasks."""
Expand All @@ -48,7 +51,6 @@ def get_setup_progress() -> dict:
"""Returns a dictionary of the status of the database setup."""
# Check if setup is currently in progress
active_setup_task, busy = get_active_setup_task()
busy = busy or is_config_task_running()
Comment thread
acwhite211 marked this conversation as resolved.

completed_resources = None
last_error = None
Expand All @@ -65,12 +67,19 @@ def get_setup_progress() -> dict:
completed_resources = get_setup_resource_progress()
last_error = get_last_setup_error()

if not busy:
setup_complete = _setup_resources_complete(completed_resources)
if active_setup_task is not None or not setup_complete:
busy = is_config_task_running()
return {
"resources": completed_resources,
"last_error": last_error,
"busy": busy,
}

def _setup_resources_complete(completed_resources: dict) -> bool:
return all(bool(resource_ready) for resource_ready in completed_resources.values())

def get_setup_resource_progress() -> dict:
"""Returns a dictionary of the status of database setup resources."""
return {
Expand Down Expand Up @@ -526,6 +535,8 @@ def get_config_progress(collection_id: Optional[int] = None) -> dict:
running_task_names = []

busy = is_config_task_running(running_task_names)
if busy and collection_id is not None:
busy = _is_new_collection_in_time_window(collection_id)
last_error = None
completed_resources = get_config_resource_progress(running_task_names)

Expand All @@ -534,3 +545,15 @@ def get_config_progress(collection_id: Optional[int] = None) -> dict:
"last_error": last_error,
"busy": busy,
}

def _is_new_collection_in_time_window(collection_id: int) -> bool:
"""Return True when a newly created collection is still in the new collection time window."""
from specifyweb.specify.models import Collection

collection = Collection.objects.filter(id=collection_id).only("timestampcreated").first()
if collection is None:
return False
if collection.timestampcreated is None:
return False

return collection.timestampcreated > timezone.now() - CONFIG_TASK_COLLECTION_BLOCK_WINDOW
36 changes: 32 additions & 4 deletions specifyweb/backend/setup_tool/schema_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
from specifyweb.celery_tasks import app
from .utils import load_json_from_file
from specifyweb.specify.models import Discipline
from django.db import transaction
from celery.exceptions import MaxRetriesExceededError

from pathlib import Path
from uuid import uuid4

import logging
logger = logging.getLogger(__name__)

SCHEMA_DEFAULTS_MISSING_DISCIPLINE_RETRY_DELAY_SEC = 2
SCHEMA_DEFAULTS_MISSING_DISCIPLINE_MAX_RETRIES = 5

def apply_schema_defaults(discipline: Discipline):
"""
Apply schema config localization defaults for this discipline.
Expand Down Expand Up @@ -59,11 +65,33 @@ def apply_schema_defaults(discipline: Discipline):

def queue_apply_schema_defaults_background(discipline_id: int) -> str:
"""Queue apply_schema_defaults to run asynchronously and return the task id."""
task = apply_schema_defaults_task.apply_async(args=[discipline_id])
return task.id
task_id = str(uuid4())

# Dispatch only after the discipline row is committed so workers can read it.
transaction.on_commit(
lambda: apply_schema_defaults_task.apply_async(
args=[discipline_id],
task_id=task_id,
)
)
return task_id

@app.task(bind=True)
@app.task(bind=True, max_retries=SCHEMA_DEFAULTS_MISSING_DISCIPLINE_MAX_RETRIES)
def apply_schema_defaults_task(self, discipline_id: int):
"""Run schema localization defaults for one discipline in a background worker."""
discipline = Discipline.objects.get(id=discipline_id)
try:
discipline = Discipline.objects.get(id=discipline_id)
except Discipline.DoesNotExist as exc:
try:
raise self.retry(
exc=exc,
countdown=SCHEMA_DEFAULTS_MISSING_DISCIPLINE_RETRY_DELAY_SEC,
)
except MaxRetriesExceededError:
logger.error(
"Skipping schema defaults. Discipline %s does not exist after %s retries.",
discipline_id,
SCHEMA_DEFAULTS_MISSING_DISCIPLINE_MAX_RETRIES,
)
return
apply_schema_defaults(discipline)