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

Tp2000 1471 task workflow #1298

Draft
wants to merge 78 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
340439c
App initial commit.
paulpepper-trade Aug 29, 2024
301644a
TP2000-1472 Revise task-related models (#1288)
dalecannon Sep 19, 2024
ee4871d
TP2000-1473 Register task-related models in admin site (#1289)
dalecannon Sep 20, 2024
9faac8b
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Sep 20, 2024
0595097
Merge branch 'master' into TP2000-1471--task-workflow
dalecannon Oct 29, 2024
de2733c
Implement prototype UI for tasks (#1308)
dalecannon Oct 31, 2024
ddfea35
TP2000-1487: Create Subtask Form (#1319)
marya-shariq Nov 11, 2024
ead85bc
TP2000-1540, TP2000-1541, TP2000-1546 & TP2000-1553 queues, workflows…
paulpepper-trade Nov 12, 2024
4d7dda9
TP2000-1543 Prototype workflow template detail view (#1322)
dalecannon Nov 12, 2024
77760e5
PR template tweaks
paulpepper-trade Nov 12, 2024
80c0b80
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Nov 12, 2024
5ec8f38
Black formatting
paulpepper-trade Nov 12, 2024
d1d18ed
TP2000-1545 Add task template reordering capability to workflow temp…
dalecannon Nov 14, 2024
84e3c16
TP2000-48 & TP2000-1549 Task template create and detail views (#1326)
paulpepper-trade Nov 15, 2024
f2bead0
TP2000-1551 Task template update views and forms (#1327)
paulpepper-trade Nov 15, 2024
d103846
Fix typo in macro import
paulpepper-trade Nov 15, 2024
d0466d4
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Nov 15, 2024
01db672
TP2000-1569 Embellish create subtask journey (#1325)
LaurenMullally Nov 18, 2024
baf7f61
Add support view support for task workflow deletion (#1329)
paulpepper-trade Nov 18, 2024
f4f351c
TP2000-1544 Prototype workflow template create & update views (#1328)
dalecannon Nov 18, 2024
bc787f8
Fix double quotes lint error from create subtask form pr
LaurenMullally Nov 18, 2024
a28e7bd
TP2000-1573 Implement workflow template delete view (#1330)
dalecannon Nov 19, 2024
2ff7858
Tp 2000 1489 delete subtask (#1332)
marya-shariq Nov 21, 2024
e9a4546
deleted comments
marya-shariq Nov 21, 2024
993ec64
TP2000-1488 Edit Subtasks (#1334)
LaurenMullally Nov 25, 2024
f55394a
Tp2000 1589 - temporary list views tile (#1338)
marya-shariq Nov 25, 2024
5884cfc
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Nov 25, 2024
7dfe059
TP2000-1590 TaskWorkflow summary info support (#1337)
paulpepper-trade Nov 25, 2024
997d36a
Update TaskWorkflowTemplate.create_task_workflow() method and unit te…
dalecannon Nov 28, 2024
4540d83
TP2000-1556 Implement workflow create view (#1340)
dalecannon Nov 28, 2024
2ea9f5c
TP2000-1555 Implement workflow detail view (#1341)
dalecannon Nov 28, 2024
708944c
TP2000-1602 Implement workflow delete view (#1342)
dalecannon Nov 28, 2024
02044ba
Factor out workflow item management helper functions into view mixin …
dalecannon Dec 4, 2024
2bafa60
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 5, 2024
442fd3e
Tp2000 1592 workflow template list view (#1348)
marya-shariq Dec 5, 2024
363bced
TP2000-1582 Enable customisable queue field name on QueueItem subcla…
dalecannon Dec 6, 2024
ec10b14
Include user param in form.save() call (#1350)
paulpepper-trade Dec 6, 2024
3e67a87
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 9, 2024
821d5e8
TP2000-1557 Implement workflow edit view (#1347)
dalecannon Dec 10, 2024
d9937d1
TP2000-1580 Task workflow combined list view pt2 (#1353)
paulpepper-trade Dec 10, 2024
31cafe9
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 10, 2024
206cc7d
TP2000-1615 Various errors (#1357)
paulpepper-trade Dec 17, 2024
ab3854f
Tp2000 1594 ordering of workflow templates list view (#1356)
marya-shariq Dec 17, 2024
10f14de
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 17, 2024
dd663ac
Add migrations.
paulpepper-trade Dec 17, 2024
5824196
Fix failing test (#1362)
paulpepper-trade Dec 17, 2024
4f054b6
Fix UI bug (#1364)
paulpepper-trade Dec 18, 2024
d1c3505
TP2000-1600 Add Subtask filter to Task admin view (#1344)
LaurenMullally Dec 19, 2024
1f94a81
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Dec 23, 2024
82a1f69
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 2, 2025
9b99475
Add simple TaskWorkflowAdmin view (#1373)
LaurenMullally Jan 3, 2025
755fac6
Tp2000 1542 workflow template admin support (#1374)
marya-shariq Jan 9, 2025
69e65e5
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 10, 2025
c6c0a43
TP2000-1652 Enhance workflow, workflow template admin views (#1381)
dalecannon Jan 15, 2025
b60a3c9
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 15, 2025
6d9e368
TP2000-1658 Correct usage of select_for_update() in QueueItem instan…
dalecannon Jan 16, 2025
676edc5
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Jan 23, 2025
12f4297
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 5, 2025
64a4db5
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 5, 2025
fab1fab
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 11, 2025
2064d09
TP2000-1583 Add task assignee update support (#1407)
paulpepper-trade Feb 11, 2025
6590627
Renumber clashing migration
paulpepper-trade Feb 11, 2025
50d8c6a
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 11, 2025
761d076
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 11, 2025
16ba584
Tp2000 1616 workflow task create view (#1363)
marya-shariq Feb 12, 2025
b1401a8
TP2000-1653 Enhance AutoCompleteField to support a broader range of …
dalecannon Feb 12, 2025
56f1d4b
Restrict factory to workbasket assignees
paulpepper-trade Feb 12, 2025
6f75f4f
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 12, 2025
280ee81
TP2000-1651 Fix sorting on list views (#1380)
dalecannon Feb 13, 2025
562a3c1
Pre-sort objects in view sorting tests to guarantee consistent ordering
dalecannon Feb 13, 2025
380d87b
UI text changes for new 'tickets' tile on homepage (#1414)
marya-shariq Feb 20, 2025
48ef119
references to subtasks removed from UI
marya-shariq Feb 20, 2025
b64597d
Revert "references to subtasks removed from UI"
marya-shariq Feb 20, 2025
32e04c9
Merge branch 'master' into TP2000-1471--task-workflow
paulpepper-trade Feb 26, 2025
fe6675a
TP2000-1725 Redesign ticket detail view (#1417)
dalecannon Feb 28, 2025
7ca53f0
Tp2000 1726 delete ticket view (#1420)
marya-shariq Feb 28, 2025
7ebc168
TP200-1722 Create New Ticket (#1419)
LaurenMullally Mar 3, 2025
57bbf8e
TP2000-1724 Redesign ticket edit view (#1421)
dalecannon Mar 5, 2025
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
Prev Previous commit
Next Next commit
TP2000-1658 Correct usage of select_for_update() in QueueItem instanc…
…e methods (#1385)

* Evaluate querysets that rely on select_for_update()

* Add race condition unit tests for QueueItem instance methods
  • Loading branch information
dalecannon authored Jan 16, 2025

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
commit 6d9e3680352e764879c13ed8fed070252791cd10
48 changes: 36 additions & 12 deletions tasks/models/queue.py
Original file line number Diff line number Diff line change
@@ -155,10 +155,18 @@ def delete(self):
queued instances up one position."""
instance = self.__class__.objects.select_for_update(nowait=True).get(pk=self.pk)

self.__class__.objects.select_for_update(nowait=True).filter(
position__gt=instance.position,
**{self.get_queue_field(): self.get_queue()},
).update(position=models.F("position") - 1)
to_update = list(
self.__class__.objects.select_for_update(nowait=True)
.filter(
position__gt=instance.position,
**{self.get_queue_field(): self.get_queue()},
)
.values_list("pk", flat=True),
)

self.__class__.objects.filter(pk__in=to_update).update(
position=models.F("position") - 1,
)

return super().delete()

@@ -234,10 +242,18 @@ def promote_to_first(self) -> Self:
if instance.position == 1:
return instance

self.__class__.objects.select_for_update(nowait=True).filter(
position__lt=instance.position,
**{self.get_queue_field(): self.get_queue()},
).update(position=models.F("position") + 1)
to_update = list(
self.__class__.objects.select_for_update(nowait=True)
.filter(
position__lt=instance.position,
**{self.get_queue_field(): self.get_queue()},
)
.values_list("pk", flat=True),
)

self.__class__.objects.filter(pk__in=to_update).update(
position=models.F("position") + 1,
)

instance.position = 1
instance.save(update_fields=["position"])
@@ -267,10 +283,18 @@ def demote_to_last(self) -> Self:
if instance.position == last_place:
return instance

self.__class__.objects.select_for_update(nowait=True).filter(
position__gt=instance.position,
**queue_kwarg,
).update(position=models.F("position") - 1)
to_update = list(
self.__class__.objects.select_for_update(nowait=True)
.filter(
position__gt=instance.position,
**queue_kwarg,
)
.values_list("pk", flat=True),
)

self.__class__.objects.filter(pk__in=to_update).update(
position=models.F("position") - 1,
)

instance.position = last_place
instance.save(update_fields=["position"])
184 changes: 184 additions & 0 deletions tasks/tests/test_queue_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import threading
from functools import wraps

import pytest
from django.db import OperationalError
from django.db.models import CASCADE
from django.db.models import ForeignKey
from django.db.models import QuerySet
from factory import SubFactory
from factory.django import DjangoModelFactory

@@ -209,3 +214,182 @@ def test_item_promote_to_first(three_item_queue):
item = item.promote_to_first()

assert item.position == 1


@pytest.mark.django_db(transaction=True)
class TestQueueRaceConditions:
"""Tests that concurrent requests to reorder queue items don't result in
duplicate or non-consecutive positions."""

NUM_THREADS: int = 2
"""The number of threads each test uses."""

THREAD_TIMEOUT: int = 5
"""The duration in seconds to wait for a thread to complete before timing
out."""

NUM_QUEUE_ITEMS: int = 5
"""The number of queue items to create for each test."""

@pytest.fixture(autouse=True)
def setup(self, queue):
"""Initialises a barrier to synchronise threads and creates queue items
anew for each test."""

self.unexpected_exceptions: list[Exception] = []

self.barrier: threading.Barrier = threading.Barrier(
parties=self.NUM_THREADS,
timeout=self.THREAD_TIMEOUT,
)

self.queue: TestQueue = queue
self.queue_items: QuerySet[TestQueueItem] = TestQueueItemFactory.create_batch(
self.NUM_QUEUE_ITEMS,
queue=queue,
)

def assert_no_unexpected_exceptions(self):
"""Asserts that no threads raised an unexpected exception."""
assert (
not self.unexpected_exceptions
), f"Unexpected exception(s) raised: {self.unexpected_exceptions}"

def assert_expected_positions(self):
"""Asserts that queue item positions remain both unique and in
consecutive sequence."""
positions = list(
TestQueueItem.objects.filter(
queue=self.queue,
)
.order_by("position")
.values_list("position", flat=True),
)

assert len(set(positions)) == len(positions), "Duplicate positions found!"

assert positions == list(
range(min(positions), max(positions) + 1),
), "Non-consecutive positions found!"

def synchronised(func):
"""
Decorator that ensures all threads wait until they can call their target
function in a synchronised fashion.

Any unexpected exceptions raised during the execution of the decorated
function are stored for the individual test to re-raise.
"""

@wraps(func)
def wrapper(self, *args, **kwargs):
try:
self.barrier.wait()
func(self, *args, **kwargs)
except OperationalError:
# A conflicting lock is already acquired
pass
except Exception as error:
self.unexpected_exceptions.append(error)

return wrapper

@synchronised
def synchronised_call(
self,
method_name: str,
queue_item: TestQueueItem,
):
"""
Thread-synchronised wrapper for the following `QueueItem` instance
methods:

- delete
- promote
- demote
- promote_to_first
- demote_to_last
"""
getattr(queue_item, method_name)()

@synchronised
def synchronised_create_queue_item(self):
"""Thread-synchronised wrapper to create a new queue item instance."""
TestQueueItemFactory.create(queue=self.queue)

def execute_threads(self, threads: list[threading.Thread]):
"""Starts a list of threads and waits for them to complete or
timeout."""
for thread in threads:
thread.start()

for thread in threads:
thread.join(timeout=self.THREAD_TIMEOUT)
if thread.is_alive():
raise RuntimeError(f"Thread {thread.name} timed out.")

def test_demote_and_promote_queue_items(self):
"""Demotes and promotes the same queue item."""
thread1 = threading.Thread(
target=self.synchronised_call,
kwargs={
"method_name": "demote",
"queue_item": self.queue_items[2],
},
name="DemoteItemThread1",
)
thread2 = threading.Thread(
target=self.synchronised_call,
kwargs={
"method_name": "promote",
"queue_item": self.queue_items[2],
},
name="PromoteItemThread2",
)

self.execute_threads([thread1, thread2])
self.assert_no_unexpected_exceptions()
self.assert_expected_positions()

def test_delete_and_create_queue_items(self):
"""Deletes the first item while creating a new one."""
thread1 = threading.Thread(
target=self.synchronised_call,
kwargs={
"method_name": "delete",
"queue_item": self.queue_items[0],
},
name="DeleteItemThread1",
)
thread2 = threading.Thread(
target=self.synchronised_create_queue_item,
name="CreateItemThread2",
)

self.execute_threads([thread1, thread2])
self.assert_no_unexpected_exceptions()
self.assert_expected_positions()

def test_promote_and_promote_to_first_queue_items(self):
"""Promotes to first the last-placed item while promoting the one before
it."""
thread1 = threading.Thread(
target=self.synchronised_call,
kwargs={
"method_name": "promote_to_first",
"queue_item": self.queue_items[4],
},
name="PromoteItemToFirstThread1",
)
thread2 = threading.Thread(
target=self.synchronised_call,
kwargs={
"method_name": "promote",
"queue_item": self.queue_items[3],
},
name="PromoteItemThread2",
)

self.execute_threads([thread1, thread2])
self.assert_no_unexpected_exceptions()
self.assert_expected_positions()
Loading