Skip to content

Commit 656a0ea

Browse files
fix: DEV-1694: Make update_tasks_states async (#2014)
* fix: DEV-1694: Make update_tasks_states async * fix: DEV-1694: Disconnect signals and update state after * refactor: DEV-1694: Update summary in delete specific tasks * fix: DEV-1693: Add disconnect a list of signals and refactor signal delete annotations from S3 * fix: DEV-1694: Move project summary recalculation to async call * fix: DEV-1694: Change delete_annotation_from_s3_storages signal to pre_delete * docs: DEV-1694: Fix example in temporary_disconnect_list_signal
1 parent 78a5751 commit 656a0ea

File tree

4 files changed

+86
-23
lines changed

4 files changed

+86
-23
lines changed

label_studio/core/utils/common.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,40 @@ def round_floats(o):
605605
if isinstance(o, (list, tuple)):
606606
return [round_floats(x) for x in o]
607607
return o
608+
609+
610+
class temporary_disconnect_list_signal:
611+
""" Temporarily disconnect a list of signals
612+
Each signal tuple: (signal_type, signal_method, object)
613+
Example:
614+
with temporary_disconnect_list_signal(
615+
[(signals.post_delete, update_is_labeled_after_removing_annotation, Annotation)]
616+
):
617+
do_something()
618+
"""
619+
def __init__(self, signals):
620+
self.signals = signals
621+
622+
def __enter__(self):
623+
for signal in self.signals:
624+
sig = signal[0]
625+
receiver = signal[1]
626+
sender = signal[2]
627+
dispatch_uid = signal[3] if len(signal) > 3 else None
628+
sig.disconnect(
629+
receiver=receiver,
630+
sender=sender,
631+
dispatch_uid=dispatch_uid
632+
)
633+
634+
def __exit__(self, type_, value, traceback):
635+
for signal in self.signals:
636+
sig = signal[0]
637+
receiver = signal[1]
638+
sender = signal[2]
639+
dispatch_uid = signal[3] if len(signal) > 3 else None
640+
sig.connect(
641+
receiver=receiver,
642+
sender=sender,
643+
dispatch_uid=dispatch_uid
644+
)

label_studio/data_manager/actions/basic.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
"""
33
import logging
44

5-
from django.db.models import signals
65
from datetime import datetime
6+
from django.db.models.signals import post_delete, pre_delete
77

88
from core.permissions import AllPermissions
99
from core.redis import start_job_async_or_sync
10-
from core.utils.common import temporary_disconnect_signal, temporary_disconnect_all_signals
10+
from core.utils.common import temporary_disconnect_list_signal
11+
from projects.models import Project
12+
1113
from tasks.models import (
12-
Annotation, Prediction, Task, update_is_labeled_after_removing_annotation,
13-
bulk_update_stats_project_tasks
14+
Annotation, Prediction, Task, bulk_update_stats_project_tasks, update_is_labeled_after_removing_annotation,
15+
update_all_task_states_after_deleting_task, remove_data_columns, remove_project_summary_annotations
1416
)
1517
from webhooks.utils import emit_webhooks_for_instance
1618
from webhooks.models import WebhookAction
@@ -38,25 +40,36 @@ def delete_tasks(project, queryset, **kwargs):
3840
"""
3941
tasks_ids = list(queryset.values('id'))
4042
count = len(tasks_ids)
43+
tasks_ids_list = [task['id'] for task in tasks_ids]
44+
# signals to switch off
45+
signals = [
46+
(post_delete, update_is_labeled_after_removing_annotation, Annotation),
47+
(post_delete, update_all_task_states_after_deleting_task, Task),
48+
(pre_delete, remove_data_columns, Task),
49+
(pre_delete, remove_project_summary_annotations, Annotation)
50+
]
4151

4252
# delete all project tasks
4353
if count == project.tasks.count():
44-
with temporary_disconnect_all_signals():
54+
with temporary_disconnect_list_signal(signals):
4555
queryset.delete()
46-
4756
project.summary.reset()
48-
project.update_tasks_states(
49-
maximum_annotations_changed=False,
50-
overlap_cohort_percentage_changed=False,
51-
tasks_number_changed=True
52-
)
5357

5458
# delete only specific tasks
5559
else:
56-
# this signal re-save the task back
57-
with temporary_disconnect_signal(signals.post_delete, update_is_labeled_after_removing_annotation, Annotation):
60+
# update project summary
61+
start_job_async_or_sync(async_project_summary_recalculation, tasks_ids_list, project.id)
62+
63+
with temporary_disconnect_list_signal(signals):
5864
queryset.delete()
5965

66+
project.update_tasks_states(
67+
maximum_annotations_changed=False,
68+
overlap_cohort_percentage_changed=False,
69+
tasks_number_changed=True
70+
)
71+
72+
# emit webhooks for project
6073
emit_webhooks_for_instance(project.organization, project, WebhookAction.TASKS_DELETED, tasks_ids)
6174

6275
# remove all tabs if there are no tasks in project
@@ -103,6 +116,13 @@ def delete_tasks_predictions(project, queryset, **kwargs):
103116
return {'processed_items': count, 'detail': 'Deleted ' + str(count) + ' predictions'}
104117

105118

119+
def async_project_summary_recalculation(tasks_ids_list, project_id):
120+
queryset = Task.objects.filter(id__in=tasks_ids_list)
121+
project = Project.objects.get(id=project_id)
122+
project.summary.remove_created_annotations_and_labels(Annotation.objects.filter(task__in=queryset))
123+
project.summary.remove_data_columns(queryset)
124+
125+
106126
actions = [
107127
{
108128
'entry_point': retrieve_tasks_predictions,

label_studio/io_storages/s3/models.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.conf import settings
1111
from django.utils.translation import gettext_lazy as _
1212
from django.dispatch import receiver
13-
from django.db.models.signals import post_save, post_delete
13+
from django.db.models.signals import post_save, post_delete, pre_delete
1414

1515
from io_storages.base_models import ImportStorage, ImportStorageLink, ExportStorage, ExportStorageLink
1616
from io_storages.utils import get_uri_via_regex
@@ -209,14 +209,14 @@ def export_annotation_to_s3_storages(sender, instance, **kwargs):
209209
storage.save_annotation(instance)
210210

211211

212-
@receiver(post_delete, sender=Annotation)
212+
@receiver(pre_delete, sender=Annotation)
213213
def delete_annotation_from_s3_storages(sender, instance, **kwargs):
214-
project = instance.task.project
215-
if hasattr(project, 'io_storages_s3exportstorages'):
216-
for storage in project.io_storages_s3exportstorages.all():
217-
if storage.can_delete_objects:
218-
logger.debug(f'Delete {instance} from S3 storage {storage}')
219-
storage.delete_annotation(instance)
214+
links = S3ExportStorageLink.objects.filter(annotation=instance)
215+
for link in links:
216+
storage = link.storage
217+
if storage.can_delete_objects:
218+
logger.debug(f'Delete {instance} from S3 storage {storage}')
219+
storage.delete_annotation(instance)
220220

221221

222222
class S3ImportStorageLink(ImportStorageLink):

label_studio/projects/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from annoying.fields import AutoOneToOneField
1313
from functools import lru_cache
1414

15+
from core.redis import start_job_async_or_sync
1516
from tasks.models import Task, Prediction, Annotation, Q_task_finished_annotations, bulk_update_stats_project_tasks
1617
from core.utils.common import create_hash, get_attr_or_item, load_func
1718
from core.utils.exceptions import LabelStudioValidationErrorSentryIgnored
@@ -344,10 +345,9 @@ def has_collaborator_enabled(self, user):
344345
membership = ProjectMember.objects.filter(user=user, project=self)
345346
return membership.exists() and membership.first().enabled
346347

347-
def update_tasks_states(
348+
def _update_tasks_states(
348349
self, maximum_annotations_changed, overlap_cohort_percentage_changed, tasks_number_changed
349350
):
350-
351351
# if only maximum annotations parameter is tweaked
352352
if maximum_annotations_changed and not overlap_cohort_percentage_changed:
353353
tasks_with_overlap = self.tasks.filter(overlap__gt=1)
@@ -371,6 +371,12 @@ def update_tasks_states(
371371
self.tasks.filter(Q(annotations__isnull=False) & Q(annotations__ground_truth=False))
372372
)
373373

374+
def update_tasks_states(
375+
self, maximum_annotations_changed, overlap_cohort_percentage_changed, tasks_number_changed
376+
):
377+
start_job_async_or_sync(self._update_tasks_states, maximum_annotations_changed, overlap_cohort_percentage_changed, tasks_number_changed)
378+
379+
374380
def _rearrange_overlap_cohort(self):
375381
"""
376382
Rearrange overlap depending on annotation count in tasks

0 commit comments

Comments
 (0)