Fix manual Status Update runs with multiple candidate integration configs.
Review Request #12835 — Created Feb. 21, 2023 and submitted — Latest diff uploaded
When performing a manual run of a
StatusUpdate
, the code responding to
the manual run signal had no choice but to try to find a matching
IntegrationConfig
for the review request. If multiple matches were
found, it had to pick one, and this could easily be the wrong one.To solve this, we now store the configuration ID in
StatusUpdate.extra_data
, using a private key. This is stored either
when setting the newStatusUpdate.integration_config
property, or when
creating theStatusUpdate
using
StatusUpdate.objects.create_for_integration()
(which is the preferred
way going forward).When either setting or fetching the configuration, the configuration is
validated to make sure it matches the sameLocalSite
, to avoid any
cross-site contamination issues.The
status_update_request_run
signal is now given aconfig
argument
mapping to this stored configuration (if one is stored), which
integrations can use. This will require a change for integrations
handling that signal to prioritize the new argument.Old Status Updates in manual run mode will result in a
None
configuration, which will need to be handled through legacy means, but
all future Status Updates created after this change will have a suitable
configuration.
All unit tests pass.
Tested this along with changes in rbintegrations to make use of the
newconfig
state.This will be tested in production with an affected customer.
reviewboard/reviews/managers.py | |||
---|---|---|---|
Revision b0bc89dfac9ee6ab49dfe3cd3eb37fdc0a521f3c | New Change | ||
1 |
"""Managers for reviewboard.reviews.models."""
|
1 |
"""Managers for reviewboard.reviews.models."""
|
2 | 2 | ||
3 |
from __future__ import annotations |
||
4 | |||
3 |
import logging |
5 |
import logging |
6 |
from typing import Dict, Optional, TYPE_CHECKING |
||
4 | 7 | ||
5 |
from django.contrib.auth.models import AnonymousUser, User |
8 |
from django.contrib.auth.models import AnonymousUser, User |
6 |
from django.core.exceptions import ObjectDoesNotExist |
9 |
from django.core.exceptions import ObjectDoesNotExist |
7 |
from django.db import connections, router, transaction, IntegrityError |
10 |
from django.db import connections, router, transaction, IntegrityError |
8 |
from django.db.models import Manager, Q |
11 |
from django.db.models import Manager, Q |
9 |
from django.db.models.query import QuerySet |
12 |
from django.db.models.query import QuerySet |
13 |
from django.utils.text import slugify |
||
10 |
from djblets.db.managers import ConcurrencyManager |
14 |
from djblets.db.managers import ConcurrencyManager |
11 | 15 | ||
12 |
from reviewboard.deprecation import RemovedInReviewBoard60Warning |
16 |
from reviewboard.deprecation import RemovedInReviewBoard60Warning |
13 |
from reviewboard.diffviewer.models import DiffSetHistory |
17 |
from reviewboard.diffviewer.models import DiffSetHistory |
14 |
from reviewboard.scmtools.errors import ChangeNumberInUseError |
18 |
from reviewboard.scmtools.errors import ChangeNumberInUseError |
15 |
from reviewboard.scmtools.models import Repository |
19 |
from reviewboard.scmtools.models import Repository |
16 |
from reviewboard.site.models import LocalSite |
20 |
from reviewboard.site.models import LocalSite |
17 | 21 | ||
22 |
if TYPE_CHECKING: |
||
23 |
from reviewboard.changedescs.models import ChangeDescription |
||
24 |
from reviewboard.integrations.base import Integration |
||
25 |
from reviewboard.integrations.models import IntegrationConfig |
||
26 |
from reviewboard.reviews.models import ReviewRequest, StatusUpdate |
||
27 | |||
18 | 28 | ||
19 |
logger = logging.getLogger(__name__) |
29 |
logger = logging.getLogger(__name__) |
20 | 30 | ||
21 | 31 | ||
22 |
class CommentManager(ConcurrencyManager): |
32 |
class CommentManager(ConcurrencyManager): |
1539 lines | |||
def _query(self, user=None, public=None, status='P', extra_query=None,
|
|||
1562 | 1572 | ||
1563 |
if distinct: |
1573 |
if distinct: |
1564 |
queryset = queryset.distinct() |
1574 |
queryset = queryset.distinct() |
1565 | 1575 | ||
1566 |
return queryset |
1576 |
return queryset |
1577 | |||
1578 | |||
1579 |
class StatusUpdateManager(Manager): |
||
1580 |
"""A manager for StatusUpdate models. |
||
1581 | |||
1582 |
This offers conveniences around creating
|
||
1583 |
:py:class:`~reviewboard.reviews.models.status_update.StatusUpdate` models
|
||
1584 |
for custom integrations.
|
||
1585 | |||
1586 |
Version Added:
|
||
1587 |
5.0.3
|
||
1588 |
"""
|
||
1589 | |||
1590 |
def create_for_integration( |
||
1591 |
self, |
||
1592 |
integration: Integration, |
||
1593 |
*, |
||
1594 |
config: IntegrationConfig, |
||
1595 |
user: User, |
||
1596 |
review_request: ReviewRequest, |
||
1597 |
change_description: Optional[ChangeDescription] = None, |
||
1598 |
service_id: Optional[str] = None, |
||
1599 |
summary: Optional[str] = None, |
||
1600 |
description: Optional[str] = None, |
||
1601 |
state: Optional[str] = None, |
||
1602 |
can_retry: bool = False, |
||
1603 |
extra_data: Dict = {}, |
||
1604 |
starting_description: str = 'starting...', |
||
1605 |
waiting_description: str = 'waiting to run.', |
||
1606 |
**kwargs, |
||
1607 |
) -> StatusUpdate: |
||
1608 |
"""Return a new status update for a given integration. |
||
1609 | |||
1610 |
This helps with generating defaults for a status update, and putting
|
||
1611 |
it in the correct initial state when running manually.
|
||
1612 | |||
1613 |
The integration configuration will be associated with the status
|
||
1614 |
update, which is important for manually running integrations when
|
||
1615 |
multiple integrations are present on a review request.
|
||
1616 | |||
1617 |
Args:
|
||
1618 |
integration (reviewboard.integrations.base.Integration):
|
||
1619 |
The integration that this status update will be associated
|
||
1620 |
with.
|
||
1621 | |||
1622 |
config (reviewboard.integrations.models.IntegrationConfig):
|
||
1623 |
The configuration for the integration, used to provide
|
||
1624 |
defaults and used for later manual runs.
|
||
1625 | |||
1626 |
user (django.contrib.auth.models.User):
|
||
1627 |
The user that the status update will be associated with.
|
||
1628 | |||
1629 |
review_request (reviewboard.reviews.models.review_request.
|
||
1630 |
ReviewRequest):
|
||
1631 |
The review request that the status update will be associated
|
||
1632 |
with.
|
||
1633 | |||
1634 |
change_description (reviewboard.changedescs.models.
|
||
1635 |
ChangeDescription, optional):
|
||
1636 |
The optional change description that the status update will
|
||
1637 |
be associated with.
|
||
1638 | |||
1639 |
service_id (str, optional):
|
||
1640 |
An explicit service ID for the status update.
|
||
1641 | |||
1642 |
If not provided (or if ``None``), a slugified version of
|
||
1643 |
the integration's name will be used.
|
||
1644 | |||
1645 |
summary (str, optional):
|
||
1646 |
An explicit summary for the status update.
|
||
1647 | |||
1648 |
If not provided (or if ``None``), the integration name will
|
||
1649 |
be used.
|
||
1650 | |||
1651 |
description (str, optional):
|
||
1652 |
An explicit description for the status update.
|
||
1653 | |||
1654 |
If not provided (or if ``None``), a standardized description
|
||
1655 |
will be used depending on whether the status update will be
|
||
1656 |
created in manual run mode.
|
||
1657 | |||
1658 |
See ``starting_description`` and ``waiting_description`` to
|
||
1659 |
customize these strings.
|
||
1660 | |||
1661 |
state (str, optional):
|
||
1662 |
An explicit state for the status update.
|
||
1663 | |||
1664 |
If not provided (or if ``None``), the state will be in Not
|
||
1665 |
Yet Run if creating in manual run mode, or Pending otherwise.
|
||
1666 | |||
1667 |
can_retry (bool, optional):
|
||
1668 |
Whether the status update can be retried after being run.
|
||
1669 | |||
1670 |
extra_data (dict, optional):
|
||
1671 |
Extra data to store in the status update.
|
||
1672 | |||
1673 |
starting_description (str, optional):
|
||
1674 |
The description to use if creating a status update that is
|
||
1675 |
immediately starting.
|
||
1676 | |||
1677 |
waiting_description (str, optional):
|
||
1678 |
The description to use if creating a status update that is
|
||
1679 |
in manual run mode.
|
||
1680 | |||
1681 |
**kwargs (dict, optional):
|
||
1682 |
Additional keyword arguments for the model.
|
||
1683 | |||
1684 |
Returns:
|
||
1685 |
reviewboard.reviews.status_update.StatusUpdate:
|
||
1686 |
The new status update.
|
||
1687 |
"""
|
||
1688 |
run_manually = config.get('run_manually') |
||
1689 | |||
1690 |
if description is None: |
||
1691 |
if run_manually: |
||
1692 |
description = waiting_description |
||
1693 |
else: |
||
1694 |
description = starting_description |
||
1695 | |||
1696 |
if state is None: |
||
1697 |
if run_manually: |
||
1698 |
state = self.model.NOT_YET_RUN |
||
1699 |
else: |
||
1700 |
state = self.model.PENDING |
||
1701 | |||
1702 |
if not service_id: |
||
1703 |
assert integration.name |
||
1704 |
service_id = slugify(integration.name) |
||
1705 | |||
1706 |
status_update: StatusUpdate = self.model( |
||
1707 |
user=user, |
||
1708 |
review_request=review_request, |
||
1709 |
change_description=change_description, |
||
1710 |
service_id=service_id, |
||
1711 |
summary=summary or integration.name, |
||
1712 |
description=description, |
||
1713 |
state=state, |
||
1714 |
extra_data=dict(extra_data, **{ |
||
1715 |
'can_retry': can_retry, |
||
1716 |
}),
|
||
1717 |
**kwargs) |
||
1718 |
status_update.integration_config = config |
||
1719 |
status_update.save() |
||
1720 | |||
1721 |
return status_update |
reviewboard/reviews/signals.py |
---|
reviewboard/reviews/models/status_update.py |
---|
reviewboard/reviews/tests/test_status_update.py |
---|
reviewboard/reviews/tests/test_status_update_manager.py |
---|